diff --git a/package-lock.json b/package-lock.json index 0bb16f2d..be72ce0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5028,6 +5028,11 @@ "@types/node": "*" } }, + "node_modules/@types/content-type": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.5.tgz", + "integrity": "sha512-dgMN+syt1xb7Hk8LU6AODOfPlvz5z1CbXpPuJE5ZrX9STfBOIXF09pEB8N7a97WT9dbngt3ksDCm6GW6yMrxfQ==" + }, "node_modules/@types/cookie": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz", @@ -5172,6 +5177,11 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, + "node_modules/@types/negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha512-c4mvXFByghezQ/eVGN5HvH/jI63vm3B7FiE81BUzDAWmuiohRecCO6ddU60dfq29oKUMiQujsoB2h0JQC7JHKA==" + }, "node_modules/@types/node": { "version": "18.11.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.17.tgz", @@ -22477,6 +22487,8 @@ "license": "MIT", "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", + "@types/content-type": "^1.1.5", + "@types/negotiator": "^0.6.1", "@types/qs": "^6.9.7", "@whook/http-transaction": "^11.0.1", "ajv": "^8.11.2", @@ -26853,6 +26865,11 @@ "@types/node": "*" } }, + "@types/content-type": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.5.tgz", + "integrity": "sha512-dgMN+syt1xb7Hk8LU6AODOfPlvz5z1CbXpPuJE5ZrX9STfBOIXF09pEB8N7a97WT9dbngt3ksDCm6GW6yMrxfQ==" + }, "@types/cookie": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz", @@ -26997,6 +27014,11 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, + "@types/negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha512-c4mvXFByghezQ/eVGN5HvH/jI63vm3B7FiE81BUzDAWmuiohRecCO6ddU60dfq29oKUMiQujsoB2h0JQC7JHKA==" + }, "@types/node": { "version": "18.11.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.17.tgz", @@ -27589,6 +27611,8 @@ "version": "file:packages/whook-http-router", "requires": { "@apidevtools/swagger-parser": "^10.1.0", + "@types/content-type": "^1.1.5", + "@types/negotiator": "^0.6.1", "@types/qs": "^6.9.7", "@typescript-eslint/eslint-plugin": "^5.36.0", "@typescript-eslint/parser": "^5.36.0", diff --git a/packages/whook-aws-lambda/src/wrappers/awsConsumerLambda.ts b/packages/whook-aws-lambda/src/wrappers/awsConsumerLambda.ts index f3f35345..45275d63 100644 --- a/packages/whook-aws-lambda/src/wrappers/awsConsumerLambda.ts +++ b/packages/whook-aws-lambda/src/wrappers/awsConsumerLambda.ts @@ -52,7 +52,7 @@ type ConsumerWrapperDependencies = { }; export default function wrapHandlerForAWSConsumerLambda< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >( initHandler: ServiceInitializer, @@ -70,7 +70,7 @@ export default function wrapHandlerForAWSConsumerLambda< } async function initHandlerForAWSConsumerLambda< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >(initHandler: ServiceInitializer, services: D): Promise { const handler: S = await initHandler(services); diff --git a/packages/whook-aws-lambda/src/wrappers/awsCronLambda.ts b/packages/whook-aws-lambda/src/wrappers/awsCronLambda.ts index a94e357b..ec22d5bd 100644 --- a/packages/whook-aws-lambda/src/wrappers/awsCronLambda.ts +++ b/packages/whook-aws-lambda/src/wrappers/awsCronLambda.ts @@ -29,7 +29,7 @@ export type LambdaCronInput = { export type LambdaCronOutput = WhookResponse; export default function wrapHandlerForAWSCronLambda< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >( initHandler: ServiceInitializer, @@ -47,7 +47,7 @@ export default function wrapHandlerForAWSCronLambda< } async function initHandlerForAWSCronLambda< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >(initHandler: ServiceInitializer, services: D): Promise { const handler: S = await initHandler(services); diff --git a/packages/whook-aws-lambda/src/wrappers/awsHTTPLambda.ts b/packages/whook-aws-lambda/src/wrappers/awsHTTPLambda.ts index a3cba434..ac8fee3b 100644 --- a/packages/whook-aws-lambda/src/wrappers/awsHTTPLambda.ts +++ b/packages/whook-aws-lambda/src/wrappers/awsHTTPLambda.ts @@ -50,7 +50,10 @@ import type { import type { TimeService, LogService } from 'common-services'; import type { OpenAPIV3 } from 'openapi-types'; import type { Readable } from 'stream'; -import type { DereferencedParameterObject } from '@whook/http-transaction'; +import { + DereferencedParameterObject, + pickAllHeaderValues, +} from '@whook/http-transaction'; import type { APIGatewayProxyEvent, APIGatewayProxyResult, @@ -85,7 +88,7 @@ const uuidPattern = '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; export default function wrapHandlerForAWSHTTPLambda< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >( initHandler: ServiceInitializer, @@ -119,7 +122,7 @@ export default function wrapHandlerForAWSHTTPLambda< } async function initHandlerForAWSHTTPLambda( - initHandler: ServiceInitializer, WhookHandler>, + initHandler: ServiceInitializer, { OPERATION_API, WRAPPERS, @@ -434,15 +437,16 @@ async function handleForAWSHTTPLambda( }), ); + const transactionId = + pickAllHeaderValues('x-transaction-id', request.headers).filter((value) => + new RegExp(uuidPattern).test(value), + )[0] || + event.requestContext.requestId || + 'no_id'; + apm('CALL', { id: event.requestContext.requestId, - transactionId: - (request.headers['x-transaction-id'] && - new RegExp(uuidPattern).test( - request.headers['x-transaction-id'] as string, - ) - ? event.headers['x-transaction-id'] - : event.requestContext.requestId) || 'no_id', + transactionId, environment: NODE_ENV, method: event.requestContext.httpMethod, resourcePath: event.requestContext.resourcePath, @@ -494,7 +498,10 @@ async function awsRequestEventToRequest( ); const request: WhookRequest = { method: event.requestContext.httpMethod.toLowerCase(), - headers: lowerCaseHeaders(event.headers as Record), + headers: lowerCaseHeaders({ + ...event.headers, + ...event.multiValueHeaders, + }) as Record, url: event.requestContext.path + (queryStringParametersNames.length diff --git a/packages/whook-aws-lambda/src/wrappers/awsKafkaConsumerLambda.ts b/packages/whook-aws-lambda/src/wrappers/awsKafkaConsumerLambda.ts index 8641d8af..1376a580 100644 --- a/packages/whook-aws-lambda/src/wrappers/awsKafkaConsumerLambda.ts +++ b/packages/whook-aws-lambda/src/wrappers/awsKafkaConsumerLambda.ts @@ -29,7 +29,7 @@ export type LambdaKafkaConsumerOutput = WhookResponse< >; export default function wrapHandlerForAWSKafkaConsumerLambda< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >( initHandler: ServiceInitializer, @@ -47,7 +47,7 @@ export default function wrapHandlerForAWSKafkaConsumerLambda< } async function initHandlerForAWSKafkaConsumerLambda< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >(initHandler: ServiceInitializer, services: D): Promise { const handler: S = await initHandler(services); diff --git a/packages/whook-aws-lambda/src/wrappers/awsLogSubscriberLambda.ts b/packages/whook-aws-lambda/src/wrappers/awsLogSubscriberLambda.ts index 4da94f69..7dc27533 100644 --- a/packages/whook-aws-lambda/src/wrappers/awsLogSubscriberLambda.ts +++ b/packages/whook-aws-lambda/src/wrappers/awsLogSubscriberLambda.ts @@ -37,7 +37,7 @@ type LogSubscriberWrapperDependencies = { // Allow to subscribe to AWS logs // See https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/SubscriptionFilters.html export default function wrapHandlerForAWSLogSubscriberLambda< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >( initHandler: ServiceInitializer, @@ -55,7 +55,7 @@ export default function wrapHandlerForAWSLogSubscriberLambda< } async function initHandlerForAWSLogSubscriberLambda< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >(initHandler: ServiceInitializer, services: D): Promise { const handler: S = await initHandler(services); diff --git a/packages/whook-aws-lambda/src/wrappers/awsS3Lambda.ts b/packages/whook-aws-lambda/src/wrappers/awsS3Lambda.ts index e6b0fcf2..8a3842a9 100644 --- a/packages/whook-aws-lambda/src/wrappers/awsS3Lambda.ts +++ b/packages/whook-aws-lambda/src/wrappers/awsS3Lambda.ts @@ -25,7 +25,7 @@ export type LambdaS3Input = { body: S3Event['Records'] }; export type LambdaS3Output = WhookResponse; export default function wrapHandlerForAWSS3Lambda< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >( initHandler: ServiceInitializer, @@ -43,7 +43,7 @@ export default function wrapHandlerForAWSS3Lambda< } async function initHandlerForAWSS3Lambda< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >(initHandler: ServiceInitializer, services: D): Promise { const handler: S = await initHandler(services); diff --git a/packages/whook-aws-lambda/src/wrappers/awsTransformerLambda.ts b/packages/whook-aws-lambda/src/wrappers/awsTransformerLambda.ts index 3f0f21cf..8fc66050 100644 --- a/packages/whook-aws-lambda/src/wrappers/awsTransformerLambda.ts +++ b/packages/whook-aws-lambda/src/wrappers/awsTransformerLambda.ts @@ -53,7 +53,7 @@ export type LambdaTransformerOutput = WhookResponse< // See https://aws.amazon.com/fr/blogs/compute/amazon-kinesis-firehose-data-transformation-with-aws-lambda/ // See https://docs.aws.amazon.com/firehose/latest/dev/data-transformation.html export default function wrapHandlerForAWSTransformerLambda< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >( initHandler: ServiceInitializer, @@ -71,7 +71,7 @@ export default function wrapHandlerForAWSTransformerLambda< } async function initHandlerForAWSTransformerLambda< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >(initHandler: ServiceInitializer, services: D): Promise { const handler: S = await initHandler(services); diff --git a/packages/whook-cors/src/index.ts b/packages/whook-cors/src/index.ts index 840c18c0..8bd1ac01 100644 --- a/packages/whook-cors/src/index.ts +++ b/packages/whook-cors/src/index.ts @@ -50,7 +50,7 @@ export type WhookAPIOperationCORSConfig = { * @returns {Function} The handler initializer wrapped */ export function wrapHandlerWithCORS< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >( initHandler: ServiceInitializer, diff --git a/packages/whook-gcp-functions/src/wrappers/googleHTTPFunction.ts b/packages/whook-gcp-functions/src/wrappers/googleHTTPFunction.ts index a3f98e73..486e030c 100644 --- a/packages/whook-gcp-functions/src/wrappers/googleHTTPFunction.ts +++ b/packages/whook-gcp-functions/src/wrappers/googleHTTPFunction.ts @@ -66,7 +66,7 @@ const SEARCH_SEPARATOR = '?'; const PATH_SEPARATOR = '/'; export default function wrapHandlerForAWSHTTPFunction< - D extends Dependencies, + D extends Dependencies, S extends WhookHandler, >( initHandler: ServiceInitializer, @@ -98,7 +98,7 @@ export default function wrapHandlerForAWSHTTPFunction< } async function initHandlerForAWSHTTPFunction( - initHandler: ServiceInitializer, WhookHandler>, + initHandler: ServiceInitializer, { OPERATION_API, WRAPPERS, diff --git a/packages/whook-graphiql/src/index.ts b/packages/whook-graphiql/src/index.ts index 3879eb57..ce8bcc43 100644 --- a/packages/whook-graphiql/src/index.ts +++ b/packages/whook-graphiql/src/index.ts @@ -41,7 +41,7 @@ export type WhookGraphIQLDependencies = WhookGraphIQLConfig & { * @param {Function} initHTTPRouter The `httpRouter` initializer * @returns {Function} The `httpRouter` initializer wrapped */ -export default function wrapHTTPRouterWithGraphIQL>( +export default function wrapHTTPRouterWithGraphIQL( initHTTPRouter: ProviderInitializer, ): ProviderInitializer { const augmentedInitializer = alsoInject< diff --git a/packages/whook-http-router/package.json b/packages/whook-http-router/package.json index 2bfb201a..dcf81999 100644 --- a/packages/whook-http-router/package.json +++ b/packages/whook-http-router/package.json @@ -54,7 +54,9 @@ "homepage": "https://github.com/nfroidure/whook", "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", + "@types/content-type": "^1.1.5", "@types/qs": "^6.9.7", + "@types/negotiator": "^0.6.1", "@whook/http-transaction": "^11.0.1", "ajv": "^8.11.2", "ajv-formats": "^2.1.1", diff --git a/packages/whook-http-router/src/index.ts b/packages/whook-http-router/src/index.ts index c9ea9e02..acd6d4db 100644 --- a/packages/whook-http-router/src/index.ts +++ b/packages/whook-http-router/src/index.ts @@ -7,6 +7,7 @@ import { Siso } from 'siso'; import Ajv from 'ajv'; import addAJVFormats from 'ajv-formats'; import { qsStrict as strictQs } from 'strict-qs'; +import { pickFirstHeaderValue } from '@whook/http-transaction'; import { OPEN_API_METHODS, flattenOpenAPI, @@ -446,6 +447,10 @@ async function initHTTPRouter({ response.headers['content-type'] || responseSpec.contentTypes[0]; } + const responseContentType = + pickFirstHeaderValue('content-type', response.headers || {}) || + 'text/plain'; + // Check the stringifyer only when a schema is // specified and it is not a binary one const responseHasSchema = @@ -453,22 +458,17 @@ async function initHTTPRouter({ operation.responses[response.status] && operation.responses[response.status].content && operation.responses[response.status].content?.[ - response.headers['content-type'] as string + responseContentType ] && - operation.responses[response.status].content?.[ - response.headers['content-type'] as string - ].schema && - (operation.responses[response.status].content?.[ - response.headers['content-type'] as string - ].schema.type !== 'string' || + operation.responses[response.status].content?.[responseContentType] + .schema && + (operation.responses[response.status].content?.[responseContentType] + .schema.type !== 'string' || operation.responses[response.status].content?.[ - response.headers['content-type'] as string + responseContentType ].schema.format !== 'binary'); - if ( - responseHasSchema && - !STRINGIFYERS[response.headers['content-type'] as string] - ) { + if (responseHasSchema && !STRINGIFYERS[responseContentType]) { throw new YHTTPError( 500, 'E_STRINGIFYER_LACK', diff --git a/packages/whook-http-router/src/libs/body.ts b/packages/whook-http-router/src/libs/body.ts index 1e2ad950..19a06494 100644 --- a/packages/whook-http-router/src/libs/body.ts +++ b/packages/whook-http-router/src/libs/body.ts @@ -2,7 +2,11 @@ import { YHTTPError } from 'yhttperror'; import FirstChunkStream from 'first-chunk-stream'; import Stream from 'stream'; import { YError } from 'yerror'; -import type { WhookOperation, WhookResponse } from '@whook/http-transaction'; +import { + pickFirstHeaderValue, + WhookOperation, + WhookResponse, +} from '@whook/http-transaction'; import type { BodySpec } from './utils.js'; import type { OpenAPIV3 } from 'openapi-types'; import type { JsonValue } from 'type-fest'; @@ -157,7 +161,11 @@ export async function sendBody( return response; } - if (!STRINGIFYERS?.[response.headers?.['content-type'] as string]) { + const responseContentType = + pickFirstHeaderValue('content-type', response.headers || {}) || + 'text/plain'; + + if (!STRINGIFYERS?.[responseContentType]) { throw new YError( 'E_STRINGIFYER_LACK', response.headers?.['content-type'], @@ -172,9 +180,7 @@ export async function sendBody( } const stream = new Encoder(); - const content = STRINGIFYERS[response.headers?.['content-type'] as string]( - response.body as string, - ); + const content = STRINGIFYERS[responseContentType](response.body as string); stream.write(content); diff --git a/packages/whook-http-router/src/libs/utils.ts b/packages/whook-http-router/src/libs/utils.ts index 1df2d9b5..239f8961 100644 --- a/packages/whook-http-router/src/libs/utils.ts +++ b/packages/whook-http-router/src/libs/utils.ts @@ -2,12 +2,15 @@ import { YHTTPError } from 'yhttperror'; import contentType from 'content-type'; import preferredCharsets from 'negotiator/lib/charset.js'; import preferredMediaType from 'negotiator/lib/encoding.js'; +import { YError } from 'yerror'; import type { OpenAPIV3 } from 'openapi-types'; -import type { +import { WhookRequest, WhookHandler, WhookOperation, WhookResponse, + pickFirstHeaderValue, + pickAllHeaderValues, } from '@whook/http-transaction'; import type { Parameters } from 'knifecycle'; @@ -72,35 +75,58 @@ export function extractBodySpec( consumableMediaTypes: string[], consumableCharsets: string[], ): BodySpec { + const contentLengthValues = pickAllHeaderValues( + 'content-length', + request.headers, + ); const bodySpec: BodySpec = { contentType: '', - contentLength: request.headers['content-length'] - ? Number(request.headers['content-length']) + contentLength: contentLengthValues.length + ? Number(contentLengthValues[0]) : 0, charset: 'utf-8', }; if (request.headers['content-type']) { + const baseContentType = pickFirstHeaderValue( + 'content-type', + request.headers, + ); + try { - const parsedContentType = parseContentType( - request.headers['content-type'], - ); + if (typeof baseContentType === 'string') { + const parsedContentType = parseContentType(baseContentType); - bodySpec.contentType = parsedContentType.type; - if ( - parsedContentType.parameters && - parsedContentType.parameters.charset - ) { - bodySpec.charset = parsedContentType.parameters.charset.toLowerCase(); - } - if ( - parsedContentType.parameters && - parsedContentType.parameters.boundary - ) { - bodySpec.boundary = parsedContentType.parameters.boundary; + bodySpec.contentType = parsedContentType.type; + if ( + parsedContentType.parameters && + parsedContentType.parameters.charset + ) { + if ( + ['utf-8', 'utf8'].includes( + parsedContentType.parameters.charset.toLowerCase(), + ) + ) { + bodySpec.charset = 'utf-8'; + } else { + throw new YError( + 'E_UNSUPPORTED_CHARSET', + parsedContentType.parameters.charset, + ); + } + } + if ( + parsedContentType.parameters && + parsedContentType.parameters.boundary + ) { + bodySpec.boundary = parsedContentType.parameters.boundary; + } + } else { + throw new YError('E_EMPTY_CONTENT_TYPE'); } } catch (err) { - throw new YHTTPError( + throw YHTTPError.wrap( + err as Error, 400, 'E_BAD_CONTENT_TYPE', request.headers['content-type'], @@ -156,10 +182,13 @@ export function extractResponseSpec( supportedMediaTypes: string[], supportedCharsets: string[], ): ResponseSpec { - const accept = (request.headers.accept as string) || '*'; + const accept = pickFirstHeaderValue('accept', request.headers) || '*'; const responseSpec: ResponseSpec = { charsets: request.headers['accept-charset'] - ? preferredCharsets(request.headers['accept-charset'], supportedCharsets) + ? preferredCharsets( + pickFirstHeaderValue('accept-charset', request.headers), + supportedCharsets, + ) : supportedCharsets, contentTypes: preferredMediaType( accept.replace(/(^|,)\*\/\*($|,|;)/g, '$1*$2'), diff --git a/packages/whook-http-transaction/API.md b/packages/whook-http-transaction/API.md index 94caac4b..23e4573f 100644 --- a/packages/whook-http-transaction/API.md +++ b/packages/whook-http-transaction/API.md @@ -5,6 +5,12 @@
initHTTPTransaction(services)Promise.<WhookHTTPTransaction>

Instantiate the httpTransaction service

+
pickFirstHeaderValue(name, headers)string
+

Pick the first header value if exists

+
+
pickAllHeaderValues(name, headers)Array
+

Pick header values

+
initAPM(services)Promise.<Object>

Application monitoring service that simply log stringified contents.

@@ -63,6 +69,32 @@ transaction created in an array. | req | HTTPRequest | A raw NodeJS HTTP incoming message | | res | HTTPResponse | A raw NodeJS HTTP response | + + +## pickFirstHeaderValue(name, headers) ⇒ string +Pick the first header value if exists + +**Kind**: global function +**Returns**: string - The value if defined. + +| Param | Type | Description | +| --- | --- | --- | +| name | string | The header name | +| headers | Object | The headers map | + + + +## pickAllHeaderValues(name, headers) ⇒ Array +Pick header values + +**Kind**: global function +**Returns**: Array - The values in an array. + +| Param | Type | Description | +| --- | --- | --- | +| name | string | The header name | +| headers | Object | The headers map | + ## initAPM(services) ⇒ Promise.<Object> diff --git a/packages/whook-http-transaction/README.md b/packages/whook-http-transaction/README.md index 5cc3bd3a..c232e209 100644 --- a/packages/whook-http-transaction/README.md +++ b/packages/whook-http-transaction/README.md @@ -34,6 +34,12 @@ This service is intended to build those objects from Node HTTP ones
initHTTPTransaction(services)Promise.<WhookHTTPTransaction>

Instantiate the httpTransaction service

+
pickFirstHeaderValue(name, headers)string
+

Pick the first header value if exists

+
+
pickAllHeaderValues(name, headers)Array
+

Pick header values

+
initAPM(services)Promise.<Object>

Application monitoring service that simply log stringified contents.

@@ -92,6 +98,32 @@ transaction created in an array. | req | HTTPRequest | A raw NodeJS HTTP incoming message | | res | HTTPResponse | A raw NodeJS HTTP response | + + +## pickFirstHeaderValue(name, headers) ⇒ string +Pick the first header value if exists + +**Kind**: global function +**Returns**: string - The value if defined. + +| Param | Type | Description | +| --- | --- | --- | +| name | string | The header name | +| headers | Object | The headers map | + + + +## pickAllHeaderValues(name, headers) ⇒ Array +Pick header values + +**Kind**: global function +**Returns**: Array - The values in an array. + +| Param | Type | Description | +| --- | --- | --- | +| name | string | The header name | +| headers | Object | The headers map | + ## initAPM(services) ⇒ Promise.<Object> diff --git a/packages/whook-http-transaction/src/index.ts b/packages/whook-http-transaction/src/index.ts index 4a3597b9..6be41765 100644 --- a/packages/whook-http-transaction/src/index.ts +++ b/packages/whook-http-transaction/src/index.ts @@ -4,13 +4,13 @@ import statuses from 'statuses'; import ms from 'ms'; import initObfuscatorService from './services/obfuscator.js'; import initAPMService from './services/apm.js'; +import { printStackTrace, YError } from 'yerror'; import type { Parameters, HandlerFunction, Dependencies } from 'knifecycle'; import type { LogService, TimeService, DelayService } from 'common-services'; import type { IncomingMessage, ServerResponse } from 'http'; import type { OpenAPIV3 } from 'openapi-types'; import type { JsonValue } from 'type-fest'; import type { Readable } from 'stream'; -import { printStackTrace, YError } from 'yerror'; import type { ObfuscatorService, ObfuscatorConfig, @@ -276,7 +276,7 @@ async function initHTTPTransaction({ headers: Object.keys(req.headers).reduce( (finalHeaders, name) => ({ ...finalHeaders, - [name]: _pickFirstHeaderValue(name, req.headers), + [name]: req.headers[name], }), {}, ), @@ -290,7 +290,7 @@ async function initHTTPTransaction({ protocol: 'http', ip: '' + - (_pickFirstHeaderValue('x-forwarded-for', req.headers) || '').split( + (pickFirstHeaderValue('x-forwarded-for', req.headers) || '').split( ',', )[0] || req.socket.remoteAddress || @@ -310,7 +310,7 @@ async function initHTTPTransaction({ * @name id */ let id: string = - _pickFirstHeaderValue('transaction-id', req.headers) || uniqueId(); + pickFirstHeaderValue('transaction-id', req.headers) || uniqueId(); // Handle bad client transaction ids if (FINAL_TRANSACTIONS[id]) { @@ -494,10 +494,37 @@ async function initHTTPTransaction({ } } -function _pickFirstHeaderValue( +/** + * Pick the first header value if exists + * @function + * @param {string} name + * The header name + * @param {Object} headers + * The headers map + * @return {string} + * The value if defined. + */ +export function pickFirstHeaderValue( name: string, headers: IncomingMessage['headers'], ): string | undefined { + return pickAllHeaderValues(name, headers)[0]; +} + +/** + * Pick header values + * @function + * @param {string} name + * The header name + * @param {Object} headers + * The headers map + * @return {Array} + * The values in an array. + */ +export function pickAllHeaderValues( + name: string, + headers: IncomingMessage['headers'], +): string[] { const headerValues: string[] = headers && typeof headers[name] === 'undefined' ? [] @@ -505,5 +532,5 @@ function _pickFirstHeaderValue( ? [headers[name] as string] : (headers[name] as string[]); - return headerValues[0]; + return headerValues; } diff --git a/packages/whook-method-override/src/index.ts b/packages/whook-method-override/src/index.ts index 7064f6c7..342417d4 100644 --- a/packages/whook-method-override/src/index.ts +++ b/packages/whook-method-override/src/index.ts @@ -1,4 +1,5 @@ import { wrapInitializer, alsoInject } from 'knifecycle'; +import { pickFirstHeaderValue } from '@whook/http-transaction'; import type { ServiceInitializer, Dependencies } from 'knifecycle'; import type { HTTPTransactionService } from '@whook/whook'; import type { LogService } from 'common-services'; @@ -12,7 +13,7 @@ import type { ServerResponse, IncomingMessage } from 'http'; * @returns {Function} The handler initializer wrapped */ export default function wrapHTTPTransactionWithMethodOverride< - D extends Dependencies, + D extends Dependencies, >( initHTTPTransaction: ServiceInitializer, ): ServiceInitializer { @@ -36,14 +37,16 @@ export default function wrapHTTPTransactionWithMethodOverride< res: ServerResponse, ): Promise => { const { request, transaction } = await httpTransaction(req, res); + const xHTTPMethodOverride = pickFirstHeaderValue( + 'x-http-method-override', + request.headers, + ); return { request: { ...request, - method: request.headers['x-http-method-override'] - ? ( - request.headers['x-http-method-override'] as string - ).toLowerCase() + method: xHTTPMethodOverride + ? xHTTPMethodOverride.toLowerCase() : request.method, headers: Object.keys(request.headers) .filter((headerName) => 'x-http-method-override' === headerName) diff --git a/packages/whook-swagger-ui/src/index.ts b/packages/whook-swagger-ui/src/index.ts index cbb668a1..4ec45166 100644 --- a/packages/whook-swagger-ui/src/index.ts +++ b/packages/whook-swagger-ui/src/index.ts @@ -41,9 +41,7 @@ export type WhookAPIOperationSwaggerConfig = { * @param {Function} initHTTPRouter The `httpRouter` initializer * @returns {Function} The `httpRouter` initializer wrapped */ -export default function wrapHTTPRouterWithSwaggerUI< - D extends Dependencies, ->( +export default function wrapHTTPRouterWithSwaggerUI( initHTTPRouter: ProviderInitializer, ): ProviderInitializer { const augmentedInitializer = alsoInject< diff --git a/packages/whook-versions/src/index.ts b/packages/whook-versions/src/index.ts index 8c1d2097..7268e8b6 100644 --- a/packages/whook-versions/src/index.ts +++ b/packages/whook-versions/src/index.ts @@ -39,7 +39,7 @@ export type VersionsConfig = { * @param {Function} initHandler The handler initializer * @returns {Function} The handler initializer wrapped */ -export function wrapHandlerWithVersionChecker, S>( +export function wrapHandlerWithVersionChecker( initHandler: ServiceInitializer, ): ServiceInitializer { const augmentedInitializer = alsoInject( diff --git a/packages/whook/ARCHITECTURE.md b/packages/whook/ARCHITECTURE.md index 60add896..d0dd9a63 100644 --- a/packages/whook/ARCHITECTURE.md +++ b/packages/whook/ARCHITECTURE.md @@ -189,7 +189,7 @@ The default Whook autoloader provides a simple way to load the constants, services and handlers of a Whook project automatically from the installed whook plugins. -[See in context](./src/services/_autoload.ts#L63-L67) +[See in context](./src/services/_autoload.ts#L59-L63) @@ -202,7 +202,7 @@ Loading the configuration files is done according to the `NODE_ENV` Let's load the configuration files as a convenient way to create constants on the fly -[See in context](./src/services/_autoload.ts#L192-L199) +[See in context](./src/services/_autoload.ts#L188-L195) @@ -212,7 +212,7 @@ We cannot inject the `WRAPPERS` in the auto loader when it is dynamically loaded so giving a second chance here for `WRAPPERS` to be set. -[See in context](./src/services/_autoload.ts#L130-L134) +[See in context](./src/services/_autoload.ts#L126-L130) @@ -239,7 +239,7 @@ We cannot inject the `API` in the auto loader since it is dynamically loaded so doing this during the auto loader initialization. -[See in context](./src/services/_autoload.ts#L114-L118) +[See in context](./src/services/_autoload.ts#L110-L114) @@ -276,7 +276,7 @@ In such a hard life, Whook's make it simple to First of all the autoloader looks for constants in the previously loaded `CONFIGS` configurations hash. -[See in context](./src/services/_autoload.ts#L207-L210) +[See in context](./src/services/_autoload.ts#L203-L206) @@ -285,7 +285,7 @@ First of all the autoloader looks for constants in the Here, we build the handlers map needed by the router by injecting every handler required by the API. -[See in context](./src/services/_autoload.ts#L225-L228) +[See in context](./src/services/_autoload.ts#L221-L224) @@ -294,7 +294,7 @@ Here, we build the handlers map needed by the router by injecting every Finally, we either require the handler/service module if none of the previous strategies applyed. -[See in context](./src/services/_autoload.ts#L253-L256) +[See in context](./src/services/_autoload.ts#L249-L252) @@ -302,7 +302,7 @@ Finally, we either require the handler/service module if Whook exports a `WhookInitializerMap` type to help you ensure yours are valid. -[See in context](./src/services/_autoload.ts#L39-L41) +[See in context](./src/services/_autoload.ts#L36-L38) @@ -311,7 +311,7 @@ Whook exports a `WhookInitializerMap` type to help you ensure yours are valid. In order to be able to substituate easily a service per another one can specify a mapping between a service and its substitution. -[See in context](./src/services/_autoload.ts#L179-L182) +[See in context](./src/services/_autoload.ts#L175-L178) @@ -319,7 +319,7 @@ In order to be able to substituate easily a service per another Whook exports a `WhookServiceMap` type to help you ensure yours are valid. -[See in context](./src/services/_autoload.ts#L35-L37) +[See in context](./src/services/_autoload.ts#L32-L34) @@ -328,7 +328,7 @@ Whook exports a `WhookServiceMap` type to help you ensure yours are valid. In order to be able to load a service from a given path map one can directly specify a path to use for its resolution. -[See in context](./src/services/_autoload.ts#L288-L291) +[See in context](./src/services/_autoload.ts#L284-L287) diff --git a/packages/whook/src/services/_autoload.ts b/packages/whook/src/services/_autoload.ts index a52e0956..a22a12ae 100644 --- a/packages/whook/src/services/_autoload.ts +++ b/packages/whook/src/services/_autoload.ts @@ -26,10 +26,7 @@ import type { ResolveService } from './resolve.js'; export const HANDLER_REG_EXP = /^(head|get|put|post|delete|options|handle)[A-Z][a-zA-Z0-9]+/; -export interface WhookWrapper< - D extends Dependencies, - S extends WhookHandler, -> { +export interface WhookWrapper { (initializer: Initializer): Initializer; } /* Architecture Note #5.7.1: WhookServiceMap @@ -47,18 +44,17 @@ export type AutoloadConfig = { INITIALIZER_PATH_MAP?: WhookInitializerMap; PROJECT_SRC?: string; }; -export type AutoloadDependencies> = - AutoloadConfig & { - PROJECT_SRC: string; - CONFIGS?: CONFIGSService; - WRAPPERS?: WhookWrapper[]; - $injector: Injector; - importer: ImporterService<{ - default: Initializer; - }>; - resolve: ResolveService; - log?: LogService; - }; +export type AutoloadDependencies = AutoloadConfig & { + PROJECT_SRC: string; + CONFIGS?: CONFIGSService; + WRAPPERS?: WhookWrapper[]; + $injector: Injector; + importer: ImporterService<{ + default: Initializer; + }>; + resolve: ResolveService; + log?: LogService; +}; /* Architecture Note #5: `$autoload` service The default Whook autoloader provides a simple way to @@ -92,7 +88,7 @@ export default singleton(name('$autoload', autoService(initAutoload))); * @return {Promise} * A promise of the autoload function. */ -async function initAutoload>({ +async function initAutoload({ PROJECT_SRC, WHOOK_PLUGINS_PATHS = [], $injector,