@@ -4,7 +4,7 @@ import {constants} from 'http2';
import * as v from 'valibot';
import {
AllowedMiddlewareSpecification,
BackendState,
BackendState, getAllowString,
Middleware,
RequestContext,
RequestDecorator,
@@ -324,10 +324,6 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
};
const processRequest = (middlewares: AllowedMiddlewareSpecification[]) => async (req: ResourceRequestContext) => {
if (req.url === '/' || req.url === '') {
return handleGetRoot(req, theRes);
}
const { resource } = req;
if (typeof resource === 'undefined') {
throw new ErrorPlainResponse('resourceNotFound', {
@@ -414,21 +410,26 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
try {
serialized = mediaType.serialize(body);
} catch (cause) {
res.statusMessage = language.statusMessages['unableToSerializeResponse']?.replace(
/\$RESOURCE/g,
resourceReq.resource.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end(language.bodies['unableToSerializeResponse']);
handleError(
new ErrorPlainResponse('unableToSerializeResponse', {
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
cause,
})
)(resourceReq, res);
return;
}
try {
encoded = typeof serialized !== 'undefined' ? charset.encode(serialized) : undefined;
} catch (cause) {
res.statusMessage = language.statusMessages['unableToEncodeResponse']?.replace(/\$RESOURCE/g,
resourceReq.resource.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end(language.bodies['unableToEncodeResponse']);
handleError(
new ErrorPlainResponse('unableToEncodeResponse', {
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
cause,
})
)(resourceReq, res);
return;
}
@@ -469,6 +470,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
try {
serialized = mediaType.serialize(body);
} catch (cause) {
// TODO logging
res.statusMessage = language.statusMessages['unableToSerializeResponse'];
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end();
@@ -478,6 +480,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
try {
encoded = typeof serialized !== 'undefined' ? charset.encode(serialized) : undefined;
} catch (cause) {
// TODO logging
res.statusMessage = language.statusMessages['unableToEncodeResponse'];
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
res.end();
@@ -503,15 +506,188 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
res.end();
};
const handleResourceResponse = (resourceReq: ResourceRequestContext, res: http.ServerResponse<RequestContext>) => (middlewareState: Response) => {
const language = resourceReq.cn.language ?? resourceReq.backend.cn.language;
const mediaType = resourceReq.cn.mediaType ?? resourceReq.backend.cn.mediaType;
const charset = resourceReq.cn.charset ?? resourceReq.backend.cn.charset;
const headers: Record<string, string> = {
...(
middlewareState.headers ?? {}
),
'Content-Language': 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') {
let serialized;
try {
serialized = mediaType.serialize(middlewareState.body);
} catch (cause) {
const headers: Record<string, string> = {
'Content-Language': language.name,
};
if (resourceReq.method === 'POST') {
headers['Accept-Post'] = Array.from(resourceReq.backend.app.mediaTypes.keys())
.filter((t) => !Object.keys(PATCH_CONTENT_MAP_TYPE).includes(t))
.join(',');
} else if (resourceReq.method === 'PATCH') {
headers['Accept-Patch'] = Array.from(Object.entries(PATCH_CONTENT_MAP_TYPE))
.filter(([, value]) => Object.keys(resourceReq.resource.state.canPatch).includes(value))
.map(([contentType]) => contentType)
.join(',');
}
handleError(new ErrorPlainResponse('unableToSerializeResponse', {
cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
headers,
res,
}))(resourceReq, res);
return;
}
try {
encoded = charset.encode(serialized);
} catch (cause) {
handleError(new ErrorPlainResponse('unableToEncodeResponse', {
cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
headers,
res,
}))(resourceReq, res);
return;
}
headers['Content-Type'] = [
mediaType.name,
`charset=${charset.name}`,
].join('; ');
}
const statusMessageKey = middlewareState.statusMessage ? 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;
}
res.end();
return;
}
handleError(new ErrorPlainResponse('urlNotFound', {
statusCode: constants.HTTP_STATUS_NOT_FOUND,
res,
}))(resourceReq, res);
};
const handleResponse = (resourceReq: RequestContext, res: http.ServerResponse<RequestContext>) => (middlewareState: Response) => {
if ('resource' in resourceReq && typeof resourceReq.resource !== 'undefined') {
handleResourceResponse(resourceReq as ResourceRequestContext, res)(middlewareState);
return;
}
const language = resourceReq.cn.language ?? resourceReq.backend.cn.language;
const mediaType = resourceReq.cn.mediaType ?? resourceReq.backend.cn.mediaType;
const charset = resourceReq.cn.charset ?? resourceReq.backend.cn.charset;
const headers: Record<string, string> = {
...(
middlewareState.headers ?? {}
),
'Content-Language': 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') {
let serialized;
try {
serialized = mediaType.serialize(middlewareState.body);
} catch (cause) {
const headers: Record<string, string> = {
'Content-Language': language.name,
};
if (resourceReq.method === 'POST') {
headers['Accept-Post'] = Array.from(resourceReq.backend.app.mediaTypes.keys())
.filter((t) => !Object.keys(PATCH_CONTENT_MAP_TYPE).includes(t))
.join(',');
}
handleError(new ErrorPlainResponse('unableToSerializeResponse', {
cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
headers,
res,
}))(resourceReq, res);
return;
}
try {
encoded = charset.encode(serialized);
} catch (cause) {
handleError(new ErrorPlainResponse('unableToEncodeResponse', {
cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
headers,
res,
}))(resourceReq, res);
return;
}
headers['Content-Type'] = [
mediaType.name,
`charset=${charset.name}`,
].join('; ');
}
const statusMessageKey = middlewareState.statusMessage ? language.statusMessages[middlewareState.statusMessage] : undefined;
res.statusMessage = statusMessageKey ?? '';
res.writeHead(middlewareState.statusCode, headers);
if (typeof encoded !== 'undefined') {
res.end(encoded);
return;
}
res.end();
return;
}
handleError(new ErrorPlainResponse('urlNotFound', {
statusCode: constants.HTTP_STATUS_NOT_FOUND,
res,
}))(resourceReq, res);
};
const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse<RequestContext>) => {
const plainReq = await decorateRequest(reqRaw); // TODO add type safety here, put handleGetRoot as its own middleware as it does not concern over any resource
const language = plainReq.cn.language ?? plainReq.backend.cn.language;
const mediaType = plainReq.cn.mediaType ?? plainReq.backend.cn.mediaType;
const charset = plainReq.cn.charset ?? plainReq.backend.cn.charset;
const plainReq = await decorateRequest(reqRaw); // TODO add type safety here
if (plainReq.url === '/' || plainReq.url === '') {
const response = await handleGetRoot(plainReq as ResourceRequestContext, theRes);
if (typeof response === 'undefined') {
handleError(
new ErrorPlainResponse('internalServerError', {
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
})
)(reqRaw, res);
return;
}
handleResponse(plainReq as ResourceRequestContext, res)(response);
return;
}
if (typeof plainReq.resource !== 'undefined') {
const resourceReq = plainReq as ResourceRequestContext;
// TODO custom middlewares
const effectiveMiddlewares = (
typeof resourceReq.resourceId === 'string'
@@ -530,92 +706,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
return;
}
// TODO extract to separate function
const headers: Record<string, string> = {
...(
middlewareState.headers ?? {}
),
'Content-Language': 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') {
let serialized;
try {
serialized = mediaType.serialize(middlewareState.body);
} catch (cause) {
const headers: Record<string, string> = {
'Content-Language': language.name,
};
if (resourceReq.method === 'POST') {
headers['Accept-Post'] = Array.from(resourceReq.backend.app.mediaTypes.keys())
.filter((t) => !Object.keys(PATCH_CONTENT_MAP_TYPE).includes(t))
.join(',');
} else if (resourceReq.method === 'PATCH') {
headers['Accept-Patch'] = Array.from(Object.entries(PATCH_CONTENT_MAP_TYPE))
.filter(([, value]) => Object.keys(resourceReq.resource.state.canPatch).includes(value))
.map(([contentType]) => contentType)
.join(',');
}
handleError(new ErrorPlainResponse('unableToSerializeResponse', {
cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
headers,
res,
}))(resourceReq, res);
return;
}
try {
encoded = charset.encode(serialized);
} catch (cause) {
handleError(new ErrorPlainResponse('unableToEncodeResponse', {
cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
headers,
res,
}))(resourceReq, res);
return;
}
headers['Content-Type'] = [
mediaType.name,
`charset=${charset.name}`,
].join('; ');
}
const statusMessageKey = middlewareState.statusMessage ? 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;
}
res.end();
return;
}
if (middlewares.length > 0) {
handleError(new ErrorPlainResponse('methodNotAllowed', {
statusCode: constants.HTTP_STATUS_METHOD_NOT_ALLOWED,
res,
headers: {
Allow: middlewares.map((m) => m.method).join(', '),
},
}))(resourceReq, res);
return;
}
handleError(new ErrorPlainResponse('urlNotFound', {
statusCode: constants.HTTP_STATUS_NOT_FOUND,
res,
}))(resourceReq, res);
handleResponse(resourceReq, res)(middlewareState);
return;
}
@@ -630,7 +721,6 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
new ErrorPlainResponse('internalServerError', {
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
body: language.bodies['internalServerError'],
})
)(reqRaw, res);
};