Browse Source

Streamline error handling

Implement default error handler, unify error handling logic in server.
master
TheoryOfNekomata 7 months ago
parent
commit
f92af9b5c8
2 changed files with 62 additions and 35 deletions
  1. +59
    -34
      packages/core/src/backend/servers/http/core.ts
  2. +3
    -1
      packages/core/src/common/resource.ts

+ 59
- 34
packages/core/src/backend/servers/http/core.ts View File

@@ -160,8 +160,11 @@ class CqrsEventEmitter extends EventEmitter {


} }


export type ErrorHandler = <E extends Error = Error>(err?: E) => never;

interface ServerState { interface ServerState {
requestDecorators: Set<RequestDecorator>; requestDecorators: Set<RequestDecorator>;
errorHandler: ErrorHandler;
} }


export interface Server { export interface Server {
@@ -170,12 +173,17 @@ export interface Server {
close(callback?: (err?: Error) => void): this; close(callback?: (err?: Error) => void): this;
listen(...args: Parameters<http.Server['listen']>): this; listen(...args: Parameters<http.Server['listen']>): this;
requestDecorator(requestDecorator: RequestDecorator): this; requestDecorator(requestDecorator: RequestDecorator): this;
defaultErrorHandler(): this;
defaultErrorHandler(errorHandler: ErrorHandler): this;
} }


const DEFAULT_ERROR_HANDLER: ErrorHandler = (err) => {
throw err;
};

export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => { export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => {
const state: ServerState = { const state: ServerState = {
requestDecorators: new Set<RequestDecorator>(), requestDecorators: new Set<RequestDecorator>(),
errorHandler: DEFAULT_ERROR_HANDLER,
}; };


const isHttps = 'key' in serverParams && 'cert' in serverParams; const isHttps = 'key' in serverParams && 'cert' in serverParams;
@@ -391,7 +399,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
); );
}; };


const handleMiddlewareError = (processRequestErrRaw: Error) => (resourceReq: ResourceRequestContext, res: http.ServerResponse<RequestContext>) => {
const handleResourceError = (processRequestErrRaw: Error) => (resourceReq: ResourceRequestContext, res: http.ServerResponse<RequestContext>) => {
const finalErr = processRequestErrRaw as ErrorPlainResponse; const finalErr = processRequestErrRaw as ErrorPlainResponse;
const headers = finalErr.headers ?? {}; const headers = finalErr.headers ?? {};
const language = resourceReq.cn.language ?? resourceReq.backend.cn.language; const language = resourceReq.cn.language ?? resourceReq.backend.cn.language;
@@ -439,6 +447,12 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
res.end(); res.end();
}; };


const handleError = (err: Error) => (req: RequestContext, res: http.ServerResponse<RequestContext>) => {
if ('resource' in req && typeof req.resource !== 'undefined') {
handleResourceError(err)(req as ResourceRequestContext, res);
}
};

const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse<RequestContext>) => { 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 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 language = plainReq.cn.language ?? plainReq.backend.cn.language;
@@ -462,7 +476,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
middlewareState = await processRequestFn(resourceReq) as any; // TODO fix this middlewareState = await processRequestFn(resourceReq) as any; // TODO fix this
} catch (processRequestErrRaw) { } catch (processRequestErrRaw) {
// TODO add error handlers // TODO add error handlers
handleMiddlewareError(processRequestErrRaw as Error)(resourceReq, res);
handleError(processRequestErrRaw as Error)(resourceReq, res);
return; return;
} }


@@ -501,23 +515,25 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
return `${mimeType}/merge-patch+${mimeSubtype}`; return `${mimeType}/merge-patch+${mimeSubtype}`;
}).join(','); }).join(',');
} }
res.statusMessage = language.statusMessages['unableToSerializeResponse']?.replace(
/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, headers);
res.end();

handleError(new ErrorPlainResponse('unableToSerializeResponse', {
cause,
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
headers,
res,
}))(resourceReq, res);
return; return;
} }


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


@@ -539,30 +555,38 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
} }


if (middlewares.length > 0) { if (middlewares.length > 0) {
res.statusMessage = 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': language.name,
});
res.end();
handleError(new ErrorPlainResponse('methodNotAllowed', {
statusCode: constants.HTTP_STATUS_METHOD_NOT_ALLOWED,
res,
headers: {
Allow: middlewares.map((m) => m.method).join(', '),
},
}))(resourceReq, res);
return; return;
} }
res.statusMessage = language.statusMessages.urlNotFound.replace(/\$RESOURCE/g,
resourceReq.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_NOT_FOUND, {
'Content-Language': language.name,
});
res.end();

handleError(new ErrorPlainResponse('urlNotFound', {
statusCode: constants.HTTP_STATUS_NOT_FOUND,
res,
}))(resourceReq, res);
return; return;
} }


res.statusMessage = language.statusMessages.notImplemented.replace(/\$RESOURCE/g,
reqRaw.resource!.state.itemName) ?? '';
res.writeHead(constants.HTTP_STATUS_NOT_IMPLEMENTED, {
'Content-Language': language.name,
});
res.end();
try {
state.errorHandler();
} catch (err) {
handleError(err as Error)(reqRaw, res);
//handleMiddlewareError(err)(reqRaw, res);
return;
}
// TODO default error handling

handleError(
new ErrorPlainResponse('urlNotFound', {
statusCode: constants.HTTP_STATUS_NOT_FOUND,
res,
})
)(reqRaw, res);
}; };


server.on('request', handleRequest); server.on('request', handleRequest);
@@ -585,7 +609,8 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr
state.requestDecorators.add(requestDecorator); state.requestDecorators.add(requestDecorator);
return this; return this;
}, },
defaultErrorHandler() {
defaultErrorHandler(errorHandler: ErrorHandler) {
state.errorHandler = errorHandler;
return this; return this;
} }
} satisfies Server; } satisfies Server;


+ 3
- 1
packages/core/src/common/resource.ts View File

@@ -79,7 +79,9 @@ export const resource = <ResourceType extends BaseResourceType = BaseResourceTyp


return { return {
get state(): ResourceState<ResourceType['name'], ResourceType['routeName']> { get state(): ResourceState<ResourceType['name'], ResourceType['routeName']> {
return Object.freeze(resourceState);
return Object.freeze({
...resourceState,
});
}, },
canFetchCollection(b = true) { canFetchCollection(b = true) {
resourceState.canFetchCollection = b; resourceState.canFetchCollection = b;


Loading…
Cancel
Save