Преглед на файлове

Better middleware matching

Filter middleware instead of adding them progressively.
master
TheoryOfNekomata преди 7 месеца
родител
ревизия
66f6fb44ee
променени са 2 файла, в които са добавени 295 реда и са изтрити 263 реда
  1. +15
    -3
      TODO.md
  2. +280
    -260
      src/backend/http/server.ts

+ 15
- 3
TODO.md Целия файл

@@ -1,15 +1,27 @@
- [ ] Integrate with other data stores
- [ ] SQLite
- [ ] PostgreSQL
- [X] Access control with resources
- [ ] Custom definitions
- [ ] Middlewares
- [ ] Request decorators
- [ ] Status messages
- [ ] Response bodies (in case of error messages)
- [ ] Content negotiation
- [ ] Language
- [X] Encoding
- [ ] Media Type
- [X] Language
- [X] Charset
- [X] Media Type
- [ ] Improve content negotiation on success/error responses (able to explicitly select language/media type/charset)
- [X] HTTPS
- [X] Date/Datetime handling (endpoints should be able to accept timestamps and ISO date/datetime strings)
- [ ] Querying items in collections
- [ ] Better URL parsing for determining target resource/resource IDs (e.g. `/api/users/3/posts/5`, `/users/3` is a query, `posts` is the target resource, `5` is the target resource ID. Different case with `/api/users/3/posts/5/attachments`)
- [ ] Tests
- [X] Happy path
- [ ] Error handling
- [ ] Implement error handling compliant to RFC 9457 - Problem Details for HTTP APIs
- [ ] Create RESTful client for frontend, server for backend (specify data sources on the server side)
- [ ] `EventEmitter` for `202 Accepted` requests (CQRS-style service)
- [ ] Implement RPC endpoints
- [ ] Implement `Vary` header (requires providing a `getHeader()` method in the request object to listen for obtained headers)
- [ ] Add example on serving data as documents using `application/html` type.

+ 280
- 260
src/backend/http/server.ts Целия файл

@@ -87,74 +87,102 @@ export interface CreateServerParams {
streamResponses?: boolean;
}

export interface Middleware<Req extends RequestContext = RequestContext> {
type ResourceRequestContext = Omit<RequestContext, 'resource'> & Required<Pick<RequestContext, 'resource'>>;

export interface Middleware<Req extends ResourceRequestContext = ResourceRequestContext> {
(req: Req): undefined | Response | Promise<undefined | Response>;
}

const getAllowedMiddlewares = <T extends v.BaseSchema>(resource?: Resource<T>, mainResourceId?: string) => {
const middlewares = [] as [string, Middleware, v.BaseSchema?][];
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

if (typeof resource === 'undefined') {
return middlewares;
}
interface AllowedMiddlewareSpecification<Schema extends v.BaseSchema = v.BaseSchema> {
method: Method;
middleware: Middleware;
constructBodySchema?: (resource: Resource<Schema>, resourceId?: string) => v.BaseSchema;
allowed: (resource: Resource<Schema>) => boolean;
}

if (typeof resource.dataSource === 'undefined') {
return middlewares;
}
const constructPostSchema = <T extends v.BaseSchema>(resource: Resource<T>) => {
return resource.schema;
};

if (typeof mainResourceId !== 'string') {
if (resource.state.canFetchCollection) {
middlewares.push(['GET', handleGetCollection]);
}
if (resource.state.canCreate) {
middlewares.push(['POST', handleCreateItem, resource.schema]);
}
return middlewares;
const constructPutSchema = <T extends v.BaseSchema>(resource: Resource<T>, mainResourceId?: string) => {
if (typeof mainResourceId === 'undefined') {
return resource.schema;
}

if (resource.state.canFetchItem) {
middlewares.push(['GET', handleGetItem]);
}
if (resource.state.canEmplace) {
const schema = resource.schema.type === 'object' ? resource.schema as unknown as v.ObjectSchema<any> : resource.schema;
const idAttr = resource.state.shared.get('idAttr') as string;
const idConfig = resource.state.shared.get('idConfig') as any;
const putSchema = (
schema.type === 'object'
? v.merge([
schema as v.ObjectSchema<any>,
v.object({
[idAttr]: v.transform(
v.any(),
input => idConfig!.serialize(input),
v.literal(mainResourceId)
)
})
])
: schema
);
middlewares.push(['PUT', handleEmplaceItem, putSchema]);
}
if (resource.state.canPatch) {
const schema = resource.schema.type === 'object' ? resource.schema as unknown as v.ObjectSchema<any> : resource.schema;
const patchSchema = (
schema.type === 'object'
? v.partial(
schema as v.ObjectSchema<any>,
(schema as v.ObjectSchema<any>).rest,
(schema as v.ObjectSchema<any>).pipe
)
: schema
);
middlewares.push(['PATCH', handlePatchItem, patchSchema]);
}
if (resource.state.canDelete) {
middlewares.push(['DELETE', handleDeleteItem]);
}
const schema = resource.schema.type === 'object' ? resource.schema as unknown as v.ObjectSchema<any> : resource.schema;
const idAttr = resource.state.shared.get('idAttr') as string;
const idConfig = resource.state.shared.get('idConfig') as any;
return (
schema.type === 'object'
? v.merge([
schema as v.ObjectSchema<any>,
v.object({
[idAttr]: v.transform(
v.any(),
input => idConfig!.serialize(input),
v.literal(mainResourceId)
)
})
])
: schema
);
};

return middlewares;
const constructPatchSchema = <T extends v.BaseSchema>(resource: Resource<T>) => {
const schema = resource.schema.type === 'object' ? resource.schema as unknown as v.ObjectSchema<any> : resource.schema;
return (
schema.type === 'object'
? v.partial(
schema as v.ObjectSchema<any>,
(schema as v.ObjectSchema<any>).rest,
(schema as v.ObjectSchema<any>).pipe
)
: schema
);
};

// TODO add a way to define custom middlewares
const defaultCollectionMiddlewares: AllowedMiddlewareSpecification[] = [
{
method: 'GET',
middleware: handleGetCollection,
allowed: (resource) => resource.state.canFetchCollection,
},
{
method: 'POST',
middleware: handleCreateItem,
allowed: (resource) => resource.state.canCreate,
constructBodySchema: constructPostSchema,
},
];

const defaultItemMiddlewares: AllowedMiddlewareSpecification[] = [
{
method: 'GET',
middleware: handleGetItem,
allowed: (resource) => resource.state.canFetchItem,
},
{
method: 'PUT',
middleware: handleEmplaceItem,
constructBodySchema: constructPutSchema,
allowed: (resource) => resource.state.canEmplace,
},
{
method: 'PATCH',
middleware: handlePatchItem,
constructBodySchema: constructPatchSchema,
allowed: (resource) => resource.state.canPatch,
},
{
method: 'DELETE',
middleware: handleDeleteItem,
allowed: (resource) => resource.state.canDelete,
},
];

export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => {
const isHttps = 'key' in serverParams && 'cert' in serverParams;

@@ -182,25 +210,108 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
].includes(mediaType)
);

const processRequest = (middlewares: [string, Middleware, v.BaseSchema?][]) => async (req: RequestContext) => {
const handleMiddlewares = async (currentHandlerState: Awaited<ReturnType<Middleware>>, currentMiddleware: AllowedMiddlewareSpecification, req: ResourceRequestContext) => {
const { method: middlewareMethod, middleware, constructBodySchema} = currentMiddleware;
const effectiveMethod = req.method === 'HEAD' ? 'GET' : req.method;

if (effectiveMethod !== middlewareMethod) {
return currentHandlerState;
}

if (typeof currentHandlerState !== 'undefined') {
return currentHandlerState;
}

if (typeof constructBodySchema === 'function') {
const bodySchema = constructBodySchema(req.resource, req.resourceId);
const contentTypeHeader = req.headers['content-type'] ?? 'application/octet-stream';
const fragments = contentTypeHeader.replace(/\s+/g, ' ').split(';');
const mediaType = fragments[0];
const charsetParam = (
fragments
.map((s) => s.trim())
.find((f) => f.startsWith('charset='))

?? (
isTextMediaType(mediaType)
? 'charset=utf-8'
: 'charset=binary'
)
);
const [_charsetKey, charsetRaw] = charsetParam.split('=').map((s) => s.trim());
const charset = (
(
(charsetRaw.startsWith('"') && charsetRaw.endsWith('"'))
|| (charsetRaw.startsWith("'") && charsetRaw.endsWith("'"))
)
? charsetRaw.slice(1, -1).trim()
: charsetRaw.trim()
)
const theBodyBuffer = await getBody(req);
const encodingPair = req.backend.app.charsets.get(charset);
if (typeof encodingPair === 'undefined') {
throw new HttpMiddlewareError('unableToDecodeResource', {
statusCode: constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE,
});
}
const deserializerPair = req.backend.app.mediaTypes.get(mediaType);
if (typeof deserializerPair === 'undefined') {
throw new HttpMiddlewareError('unableToDeserializeResource', {
statusCode: constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE,
});
}
const theBodyStr = encodingPair.decode(theBodyBuffer);
const theBody = deserializerPair.deserialize(theBodyStr);
try {
req.body = await v.parseAsync(bodySchema, theBody, {abortEarly: false, abortPipeEarly: false});
} catch (errRaw) {
const err = errRaw as v.ValiError;
// todo use error message key for each method
// TODO better error reporting, localizable messages
// TODO handle error handlers' errors
if (Array.isArray(err.issues)) {
throw new HttpMiddlewareError('invalidResource', {
statusCode: constants.HTTP_STATUS_BAD_REQUEST,
body: err.issues.map((i) => (
`${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}`
)),
});
}
}
}

const result = await middleware(req);

// HEAD is just GET without the response body
if (req.method === 'HEAD' && result instanceof PlainResponse) {
const { body: _, ...etcResult } = result;

return new PlainResponse(etcResult);
}

return result;
};

const processRequest = (middlewares: AllowedMiddlewareSpecification[]) => async (req: ResourceRequestContext) => {
if (req.url === '/' || req.url === '') {
return handleGetRoot(req);
}

if (typeof req.resource === 'undefined') {
const { resource } = req;
if (typeof resource === 'undefined') {
throw new HttpMiddlewareError('resourceNotFound', {
statusCode: constants.HTTP_STATUS_NOT_FOUND,
});
}

if (typeof req.resource.dataSource === 'undefined') {
if (typeof resource.dataSource === 'undefined') {
throw new HttpMiddlewareError('unableToInitializeResourceDataSource', {
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
});
}

try {
await req.resource.dataSource.initialize();
await resource.dataSource.initialize();
} catch (cause) {
throw new HttpMiddlewareError(
'unableToInitializeResourceDataSource',
@@ -211,87 +322,10 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
);
}

const middlewareResponse = await middlewares.reduce<unknown>(
async (currentHandlerStatePromise, currentValue) => {
const [middlewareMethod, middleware, schema] = currentValue;
try {
const currentHandlerState = await currentHandlerStatePromise;
const effectiveMethod = req.method === 'HEAD' ? 'GET' : req.method;

if (effectiveMethod !== middlewareMethod) {
return currentHandlerState;
}

if (typeof currentHandlerState !== 'undefined') {
return currentHandlerState;
}

if (schema) {
const contentTypeHeader = req.headers['content-type'] ?? 'application/octet-stream';
const fragments = contentTypeHeader.replace(/\s+/g, ' ').split(';');
const mediaType = fragments[0];
const charsetParam = (
fragments
.map((s) => s.trim())
.find((f) => f.startsWith('charset='))

?? (
isTextMediaType(mediaType)
? 'charset=utf-8'
: 'charset=binary'
)
);
const [_charsetKey, charsetRaw] = charsetParam.split('=').map((s) => s.trim());
const charset = (
(
(charsetRaw.startsWith('"') && charsetRaw.endsWith('"'))
|| (charsetRaw.startsWith("'") && charsetRaw.endsWith("'"))
)
? charsetRaw.slice(1, -1).trim()
: charsetRaw.trim()
)
const theBodyBuffer = await getBody(req);
const encodingPair = req.backend.app.charsets.get(charset);
if (typeof encodingPair === 'undefined') {
throw new HttpMiddlewareError('unableToDecodeResource', {
statusCode: constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE,
});
}
const deserializerPair = req.backend.app.mediaTypes.get(mediaType);
if (typeof deserializerPair === 'undefined') {
throw new HttpMiddlewareError('unableToDeserializeResource', {
statusCode: constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE,
});
}
const theBodyStr = encodingPair.decode(theBodyBuffer);
const theBody = deserializerPair.deserialize(theBodyStr);
req.body = await v.parseAsync(schema, theBody, { abortEarly: false, abortPipeEarly: false });
}

const result = await middleware(req);

if (req.method === 'HEAD' && result instanceof PlainResponse) {
const { body: _, ...etcResult } = result;

return Promise.resolve(new PlainResponse(etcResult));
}

return Promise.resolve(result);
} catch (errRaw) {
// todo use error message key for each method
// TODO better error reporting, localizable messages
// TODO handle error handlers' errors
if (errRaw instanceof v.ValiError && Array.isArray(errRaw.issues)) {
throw new HttpMiddlewareError('invalidResource', {
statusCode: constants.HTTP_STATUS_BAD_REQUEST,
body: errRaw.issues.map((i) => (
`${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}`
)),
});
}

throw errRaw;
}
const middlewareResponse = await middlewares.reduce<ReturnType<Middleware>>(
async (currentHandlerStatePromise, currentMiddleware) => {
const currentHandlerState = await currentHandlerStatePromise;
return await handleMiddlewares(currentHandlerState, currentMiddleware, req);
},
Promise.resolve<ReturnType<Middleware>>(undefined)
) as Awaited<ReturnType<Middleware>>;
@@ -319,135 +353,119 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
};

const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse<RequestContext>) => {
const req = await decorateRequest(reqRaw);
const middlewares = getAllowedMiddlewares(req.resource, req.resourceId);
const processRequestFn = processRequest(middlewares);
let middlewareState: Response;
try {
middlewareState = await processRequestFn(req) as any; // TODO fix this
} catch (processRequestErrRaw) {
const finalErr = processRequestErrRaw as HttpMiddlewareError;
const headers = finalErr.response.headers ?? {};
let encoded: Buffer | undefined;
let serialized;
try {
serialized = typeof finalErr.response.body !== 'undefined' ? req.backend.cn.mediaType.serialize(finalErr.response.body) : undefined;
} catch (cause) {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end();
return;
}

const plainReq = await decorateRequest(reqRaw); // TODO add type safety here, put handleGetRoot as its own middleware as it does not concern over any resource
if (typeof plainReq.resource !== 'undefined') {
const resourceReq = plainReq as ResourceRequestContext;
const effectiveMiddlewares = (
typeof resourceReq.resourceId === 'string'
? defaultItemMiddlewares
: defaultCollectionMiddlewares
);
const middlewares = effectiveMiddlewares.filter((m) => m.allowed(resourceReq.resource));
const processRequestFn = processRequest(middlewares);
let middlewareState: Response;
try {
encoded = typeof serialized !== 'undefined' ? req.backend.cn.charset.encode(serialized) : undefined;
} catch (cause) {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end();
}

headers['Content-Type'] = [
req.backend.cn.mediaType.name,
`charset=${req.backend.cn.charset.name}`
].join('; ');

const statusMessageKey = finalErr.response.statusMessage ? req.backend.cn.language.statusMessages[finalErr.response.statusMessage] : undefined;
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? '';
res.writeHead(finalErr.response.statusCode, headers);
if (typeof encoded !== 'undefined') {
res.end(encoded);
return;
}
res.end();
return;
}

const headers: Record<string, string> = {
...(
middlewareState.headers ?? {}
),
'Content-Language': req.cn.language.name
};

if (middlewareState instanceof http.ServerResponse) {
// TODO streaming responses
middlewareState.writeHead(constants.HTTP_STATUS_ACCEPTED, headers);
return;
}

if (middlewareState instanceof PlainResponse) {
let encoded: Buffer | undefined;
if (typeof middlewareState.body !== 'undefined') {
middlewareState = await processRequestFn(resourceReq) as any; // TODO fix this
} catch (processRequestErrRaw) {
const finalErr = processRequestErrRaw as HttpMiddlewareError;
const headers = finalErr.response.headers ?? {};
let encoded: Buffer | undefined;
let serialized;
try {
serialized = req.cn.mediaType.serialize(middlewareState.body);
serialized = typeof finalErr.response.body !== 'undefined' ? resourceReq.backend.cn.mediaType.serialize(finalErr.response.body) : undefined;
} catch (cause) {
res.statusMessage = req.backend.cn.language.statusMessages['unableToSerializeResponse']?.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': req.backend.cn.language.name,
});
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end();
return;
}

try {
encoded = req.cn.charset.encode(serialized);
encoded = typeof serialized !== 'undefined' ? resourceReq.backend.cn.charset.encode(serialized) : undefined;
} catch (cause) {
res.statusMessage = req.backend.cn.language.statusMessages['unableToEncodeResponse']?.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': req.backend.cn.language.name,
});
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end();
return;
}

headers['Content-Type'] = [
req.cn.mediaType.name,
`charset=${req.cn.charset.name}`
resourceReq.backend.cn.mediaType.name,
`charset=${resourceReq.backend.cn.charset.name}`,
].join('; ');
}

const statusMessageKey = middlewareState.statusMessage ? req.cn.language.statusMessages[middlewareState.statusMessage] : undefined;
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? '';
res.writeHead(middlewareState.statusCode, headers);
if (typeof encoded !== 'undefined') {
res.end(encoded);
const statusMessageKey = finalErr.response.statusMessage ? resourceReq.backend.cn.language.statusMessages[finalErr.response.statusMessage] : undefined;
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, resourceReq.resource!.state.itemName) ?? '';
res.writeHead(finalErr.response.statusCode, headers);
if (typeof encoded !== 'undefined') {
res.end(encoded);
return;
}
res.end();
return;
}
res.end();
return;
}

if (typeof middlewareState !== 'undefined') {
try {
// TODO extract to separate function
const headers: Record<string, string> = {
...(
middlewareState.headers ?? {}
),
'Content-Language': resourceReq.cn.language.name,
};
if (middlewareState instanceof http.ServerResponse) {
// TODO streaming responses
middlewareState.writeHead(constants.HTTP_STATUS_ACCEPTED, headers);
return;
} catch (finalErrRaw) {
const finalErr = finalErrRaw as HttpMiddlewareError;
const headers = finalErr.response.headers ?? {};
}
if (middlewareState instanceof PlainResponse) {
let encoded: Buffer | undefined;
let serialized;
try {
serialized = typeof finalErr.response.body !== 'undefined' ? req.backend.cn.mediaType.serialize(finalErr.response.body) : undefined;
} catch (cause) {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end();
return;
}
if (typeof middlewareState.body !== 'undefined') {
let serialized;
try {
serialized = resourceReq.cn.mediaType.serialize(middlewareState.body);
} catch (cause) {
const headers: Record<string, string> = {
'Content-Language': resourceReq.backend.cn.language.name,
};
if (resourceReq.method === 'POST') {
headers['Accept-Post'] = Array.from(resourceReq.backend.app.mediaTypes.keys()).join(',');
} else if (resourceReq.method === 'PATCH') {
headers['Accept-Patch'] = Array.from(resourceReq.backend.app.mediaTypes.keys()).map((m) => {
const [mimeType, mimeSubtype] = m.split('/');

// TODO accept only patch document type from request
// TODO implement Vary header (which headers influenced the request)
// TODO implement OPTIONS method for determining the accepted media types and languages
// TODO configure strict and lax accept behavior for content negotiation
return `${mimeType}/merge-patch+${mimeSubtype}`;
}).join(',');
}
res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToSerializeResponse']?.replace(
/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, headers);
res.end();
return;
}

try {
encoded = typeof serialized !== 'undefined' ? req.backend.cn.charset.encode(serialized) : undefined;
} catch (cause) {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end();
}
try {
encoded = resourceReq.cn.charset.encode(serialized);
} catch (cause) {
res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToEncodeResponse']?.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': resourceReq.backend.cn.language.name,
});
res.end();
return;
}

headers['Content-Type'] = [
req.backend.cn.mediaType.name,
`charset=${req.backend.cn.charset.name}`
].join('; ');
headers['Content-Type'] = [
resourceReq.cn.mediaType.name,
`charset=${resourceReq.cn.charset.name}`,
].join('; ');
}

const statusMessageKey = finalErr.response.statusMessage ? req.backend.cn.language.statusMessages[finalErr.response.statusMessage] : undefined;
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? '';
res.writeHead(finalErr.response.statusCode, headers);
const statusMessageKey = middlewareState.statusMessage ? resourceReq.cn.language.statusMessages[middlewareState.statusMessage] : undefined;
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, resourceReq.resource!.state.itemName) ?? '';
res.writeHead(middlewareState.statusCode, headers);
if (typeof encoded !== 'undefined') {
res.end(encoded);
return;
@@ -455,25 +473,27 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
res.end();
return;
}
}

if (middlewares.length > 0) {
res.statusMessage = req.backend.cn.language.statusMessages.methodNotAllowed.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED, {
Allow: middlewares.map((m) => m[0]).join(', '),
'Content-Language': req.backend.cn.language.name,
if (middlewares.length > 0) {
res.statusMessage = resourceReq.backend.cn.language.statusMessages.methodNotAllowed.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED, {
Allow: middlewares.map((m) => m.method).join(', '),
'Content-Language': resourceReq.backend.cn.language.name,
});
res.end();
return;
}
res.statusMessage = resourceReq.backend.cn.language.statusMessages.urlNotFound.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_NOT_FOUND, {
'Content-Language': resourceReq.backend.cn.language.name,
});
res.end();
return;
}

// TODO error handler in line with authentication
res.statusMessage = req.backend.cn.language.statusMessages.urlNotFound.replace(/\$RESOURCE/g, req.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_NOT_FOUND, {
'Content-Language': req.backend.cn.language.name,
});
res.end();
return;
throw new Error('Not implemented');
};

server.on('request', handleRequest);


Зареждане…
Отказ
Запис