From dfd003722e52ad469aa2b08f6c4f8521d91c6ad7 Mon Sep 17 00:00:00 2001 From: Peter van Vliet Date: Tue, 24 Sep 2024 14:56:13 +0200 Subject: [PATCH] #296: http improvements --- packages/http/src/HttpRemote.ts | 12 +++-- packages/http/src/HttpServer.ts | 54 ++++++++++++++----- packages/http/src/definitions/HeaderKeys.ts | 11 ++++ packages/http/src/definitions/HeaderValues.ts | 9 ++++ .../http/src/definitions/IgnoredHeaderKeys.ts | 4 ++ packages/runtime/src/index.ts | 1 + 6 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 packages/http/src/definitions/HeaderKeys.ts create mode 100644 packages/http/src/definitions/HeaderValues.ts create mode 100644 packages/http/src/definitions/IgnoredHeaderKeys.ts diff --git a/packages/http/src/HttpRemote.ts b/packages/http/src/HttpRemote.ts index 7426bda9..67a7397c 100644 --- a/packages/http/src/HttpRemote.ts +++ b/packages/http/src/HttpRemote.ts @@ -3,7 +3,8 @@ import { ErrorConverter, Request, Response as ResultResponse } from '@jitar/exec import { Remote } from '@jitar/services'; import { File } from '@jitar/sourcing'; -const APPLICATION_JSON = 'application/json'; +import HeaderKeys from './definitions/HeaderKeys'; +import HeaderValues from './definitions/HeaderValues'; const errorConverter = new ErrorConverter(); @@ -32,7 +33,7 @@ export default class HttpRemote implements Remote const options = { method: 'GET' }; const response = await this.#callRemote(remoteUrl, options); - const type = response.headers.get('Content-Type') || 'application/octet-stream'; + const type = response.headers.get(HeaderKeys.CONTENT_TYPE) ?? HeaderValues.APPLICATION_STREAM; const result = await response.arrayBuffer(); const content = Buffer.from(result); @@ -68,7 +69,7 @@ export default class HttpRemote implements Remote const options = { method: 'POST', - headers: { 'Content-Type': APPLICATION_JSON }, + headers: { 'Content-Type': HeaderValues.APPLICATION_JSON }, body: JSON.stringify(body) }; @@ -77,7 +78,7 @@ export default class HttpRemote implements Remote async run(request: Request): Promise { - request.setHeader('Content-Type', APPLICATION_JSON); + request.setHeader(HeaderKeys.CONTENT_TYPE, HeaderValues.APPLICATION_JSON); const versionString = request.version.toString(); const argsObject = Object.fromEntries(request.args); @@ -128,7 +129,8 @@ export default class HttpRemote implements Remote async #getResponseResult(response: Response): Promise { - const contentType = response.headers.get('X-Jitar-Content-Type'); + const contentType = response.headers.get(HeaderKeys.JITAR_CONTENT_TYPE) + ?? response.headers.get(HeaderKeys.CONTENT_TYPE); if (contentType?.includes('undefined')) { diff --git a/packages/http/src/HttpServer.ts b/packages/http/src/HttpServer.ts index e793516a..fe9c90da 100644 --- a/packages/http/src/HttpServer.ts +++ b/packages/http/src/HttpServer.ts @@ -3,14 +3,17 @@ import express, { Express, Request, Response, NextFunction } from 'express'; import { Server as Http } from 'http'; import { RunModes } from '@jitar/execution'; -import type { Server, ServerResponse } from '@jitar/runtime'; +import { Server, ServerResponse, ContentTypes } from '@jitar/runtime'; import { Validator } from '@jitar/validation'; +import HeaderKeys from './definitions/HeaderKeys'; +import IgnoredHeaderKeys from './definitions/IgnoredHeaderKeys'; +import HeaderValues from './definitions/HeaderValues'; + const DEFAULT_PORT = '3000'; const DEFAULT_BODY_LIMIT = 1024 * 200; // 200 KB -const IGNORED_QUERY_PARAMETERS = ['version']; -const IGNORED_HEADER_KEYS = ['host', 'connection', 'content-length', 'accept-encoding', 'user-agent', 'keep-alive']; +const IGNORED_QUERY_PARAMETERS = ['version']; // TODO: Refactor version to custom header export default class HttpServer { @@ -51,7 +54,7 @@ export default class HttpServer this.#app.use(express.urlencoded({ extended: true })); this.#app.use(this.#addDefaultHeaders.bind(this)); - this.#app.disable('x-powered-by'); + this.#app.disable(HeaderKeys.POWERED_BY); } #setupRoutes(): void @@ -96,7 +99,7 @@ export default class HttpServer // eslint-disable-next-line @typescript-eslint/no-unused-vars #addDefaultHeaders(request: Request, response: Response, next: NextFunction): void { - response.setHeader('X-Content-Type-Options', 'nosniff'); + response.setHeader(HeaderKeys.CONTENT_TYPE_OPTIONS, HeaderValues.NO_SNIFF); next(); } @@ -252,7 +255,7 @@ export default class HttpServer const lowerKey = key.toLowerCase(); const stringValue = value.toString(); - if (IGNORED_HEADER_KEYS.includes(lowerKey)) + if (IgnoredHeaderKeys.includes(lowerKey)) { continue; } @@ -266,10 +269,11 @@ export default class HttpServer #transformResponse(response: Response, serverResponse: ServerResponse): Response { const status = this.#transformStatus(serverResponse); + const contentType = this.#transformContentType(serverResponse); response.status(status); - response.setHeader('Content-Type', serverResponse.contentType); - response.setHeader('X-Jitar-Content-Type', serverResponse.contentType); + response.setHeader(HeaderKeys.CONTENT_TYPE, contentType); + response.setHeader(HeaderKeys.JITAR_CONTENT_TYPE, serverResponse.contentType); for (const [name, value] of Object.entries(serverResponse.headers)) { @@ -291,14 +295,36 @@ export default class HttpServer return serverResponse.status; } + #transformContentType(serverResponse: ServerResponse): string + { + const contentType = serverResponse.contentType.toLowerCase(); + + switch (contentType) + { + case ContentTypes.BOOLEAN: + case ContentTypes.NUMBER: + case ContentTypes.UNDEFINED: + case ContentTypes.NULL: + return ContentTypes.TEXT; + } + + return contentType; + } + #transformResult(serverResponse: ServerResponse): unknown { - if (typeof serverResponse.result === 'number' - || typeof serverResponse.result === 'boolean') - { - return String(serverResponse.result); - } + const result = serverResponse.result; + + // if (result === undefined || result === null) + // { + // return ''; + // } + + if (typeof result === 'number' || typeof result === 'boolean') + { + return String(result); + } - return serverResponse.result; + return result; } } diff --git a/packages/http/src/definitions/HeaderKeys.ts b/packages/http/src/definitions/HeaderKeys.ts new file mode 100644 index 00000000..d16be82a --- /dev/null +++ b/packages/http/src/definitions/HeaderKeys.ts @@ -0,0 +1,11 @@ + +const HeaderKeys = +{ + CONTENT_TYPE: 'Content-Type', + CONTENT_LENGTH: 'Content-Length', + CONTENT_TYPE_OPTIONS: 'X-Content-Type-Options', + JITAR_CONTENT_TYPE: 'X-Jitar-Content-Type', + POWERED_BY: 'X-Powered-By' +} as const; + +export default HeaderKeys; diff --git a/packages/http/src/definitions/HeaderValues.ts b/packages/http/src/definitions/HeaderValues.ts new file mode 100644 index 00000000..035c6f7d --- /dev/null +++ b/packages/http/src/definitions/HeaderValues.ts @@ -0,0 +1,9 @@ + +const HeaderValues = +{ + NO_SNIFF: 'nosniff', + APPLICATION_JSON: 'application/json', + APPLICATION_STREAM: 'application/octet-stream' +} as const; + +export default HeaderValues; diff --git a/packages/http/src/definitions/IgnoredHeaderKeys.ts b/packages/http/src/definitions/IgnoredHeaderKeys.ts new file mode 100644 index 00000000..45131545 --- /dev/null +++ b/packages/http/src/definitions/IgnoredHeaderKeys.ts @@ -0,0 +1,4 @@ + +const IgnoredHeaderKeys: string[] = ['host', 'connection', 'content-length', 'accept-encoding', 'user-agent', 'keep-alive'] as const; + +export default IgnoredHeaderKeys; diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index af80f029..c229a27d 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -2,6 +2,7 @@ export { default as Client } from './client/Client'; export { default as ClientBuilder } from './client/ClientBuilder'; +export { default as ContentTypes } from './server/definitions/ContentTypes'; export { default as ServerResponse } from './server/types/ServerResponse'; export { default as Server } from './server/Server'; export { default as ServerBuilder } from './server/ServerBuilder';