Browse Source

Fix root, error handling

Ensure root can be called with any method.
master
TheoryOfNekomata 7 months ago
parent
commit
c92f6899b2
13 changed files with 226 additions and 118 deletions
  1. +5
    -0
      packages/core/src/backend/common.ts
  2. +196
    -106
      packages/core/src/backend/servers/http/core.ts
  3. +5
    -3
      packages/core/src/backend/servers/http/handlers/default.ts
  4. +1
    -1
      packages/core/src/backend/servers/http/utils.ts
  5. +1
    -1
      packages/data-sources/file-jsonl/test/index.test.ts
  6. +1
    -1
      packages/examples/cms-web-api/bruno/Create Post with ID.bru
  7. +1
    -1
      packages/examples/cms-web-api/bruno/Delete Post.bru
  8. +11
    -0
      packages/examples/cms-web-api/bruno/Get Root.bru
  9. +1
    -1
      packages/examples/cms-web-api/bruno/Get Single Post.bru
  10. +1
    -1
      packages/examples/cms-web-api/bruno/Modify Post (Delta).bru
  11. +1
    -1
      packages/examples/cms-web-api/bruno/Modify Post (Merge).bru
  12. +1
    -1
      packages/examples/cms-web-api/bruno/Query Posts.bru
  13. +1
    -1
      packages/examples/cms-web-api/bruno/Replace Post.bru

+ 5
- 0
packages/core/src/backend/common.ts View File

@@ -66,3 +66,8 @@ export interface Response {
// metadata of the response
headers?: Record<string, string>;
}

export const getAllowString = (middlewares: AllowedMiddlewareSpecification[]) => {
const allowedMethods = middlewares.flatMap((m) => m.method === 'GET' ? [m.method, 'HEAD'] : [m.method]);
return allowedMethods.join(',');
}

+ 196
- 106
packages/core/src/backend/servers/http/core.ts View File

@@ -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);
};


+ 5
- 3
packages/core/src/backend/servers/http/handlers/default.ts View File

@@ -1,5 +1,5 @@
import {constants} from 'http2';
import {AllowedMiddlewareSpecification, Middleware} from '../../../common';
import {AllowedMiddlewareSpecification, getAllowString, Middleware} from '../../../common';
import {LinkMap} from '../utils';
import {PlainResponse, ErrorPlainResponse} from '../response';
import {getAcceptPatchString, getAcceptPostString} from '../../../../common';
@@ -43,11 +43,12 @@ export const handleGetRoot: Middleware = (req, res) => {

export const handleOptions = (middlewares: AllowedMiddlewareSpecification[]): Middleware => (req, res) => {
if (middlewares.length > 0) {
const allowedMethods = middlewares.flatMap((m) => m.method === 'GET' ? [m.method, 'HEAD'] : [m.method]);
const allowString = getAllowString(middlewares);
const headers: Record<string, string> = {
'Allow': allowedMethods.join(', '),
'Allow': getAllowString(middlewares),
};

const allowedMethods = allowString.split(',');
if (allowedMethods.includes('POST')) {
headers['Accept-Post'] = getAcceptPostString(req.backend.app.mediaTypes);
}
@@ -60,6 +61,7 @@ export const handleOptions = (middlewares: AllowedMiddlewareSpecification[]): Mi
headers['Accept-Patch'] = getAcceptPatchString(req.resource.state.canPatch);
}
}

return new PlainResponse({
headers,
statusMessage: 'provideOptions',


+ 1
- 1
packages/core/src/backend/servers/http/utils.ts View File

@@ -39,7 +39,7 @@ export class LinkMap extends Set<LinkMapEntry> {
const params = Object.entries(e.params);

return [
`<${encodeURIComponent(e.url)}>`,
`<${e.url}>`,
...params.map(([key, value]) => `${encodeURIComponent(key)}="${encodeURIComponent(value)}"`)
].join(';')
}).join(',');


+ 1
- 1
packages/data-sources/file-jsonl/test/index.test.ts View File

@@ -139,7 +139,7 @@ describe('methods', () => {
}
])
);
expect(newItem).toEqual(data);
expect(newItem).toEqual({ id: 0, ...data});
});
});



+ 1
- 1
packages/examples/cms-web-api/bruno/Create Post with ID.bru View File

@@ -1,7 +1,7 @@
meta {
name: Create Post with ID
type: http
seq: 7
seq: 8
}

put {


+ 1
- 1
packages/examples/cms-web-api/bruno/Delete Post.bru View File

@@ -1,7 +1,7 @@
meta {
name: Delete Post
type: http
seq: 8
seq: 9
}

delete {


+ 11
- 0
packages/examples/cms-web-api/bruno/Get Root.bru View File

@@ -0,0 +1,11 @@
meta {
name: Get Root
type: http
seq: 1
}

get {
url: http://localhost:6969/api
body: none
auth: none
}

+ 1
- 1
packages/examples/cms-web-api/bruno/Get Single Post.bru View File

@@ -1,7 +1,7 @@
meta {
name: Get Single Post
type: http
seq: 3
seq: 4
}

get {


+ 1
- 1
packages/examples/cms-web-api/bruno/Modify Post (Delta).bru View File

@@ -1,7 +1,7 @@
meta {
name: Modify Post (Delta)
type: http
seq: 5
seq: 6
}

patch {


+ 1
- 1
packages/examples/cms-web-api/bruno/Modify Post (Merge).bru View File

@@ -1,7 +1,7 @@
meta {
name: Modify Post (Merge)
type: http
seq: 4
seq: 5
}

patch {


+ 1
- 1
packages/examples/cms-web-api/bruno/Query Posts.bru View File

@@ -1,7 +1,7 @@
meta {
name: Query Posts
type: http
seq: 1
seq: 3
}

get {


+ 1
- 1
packages/examples/cms-web-api/bruno/Replace Post.bru View File

@@ -1,7 +1,7 @@
meta {
name: Replace Post
type: http
seq: 6
seq: 7
}

put {


Loading…
Cancel
Save