Comparing version 3.10.2 to 3.11.0
@@ -164,2 +164,3 @@ "use strict"; | ||
body: Buffer! | ||
rawTrailers: Json! | ||
} | ||
@@ -201,2 +202,3 @@ | ||
body: Buffer! | ||
rawTrailers: Json! | ||
} | ||
@@ -203,0 +205,0 @@ |
@@ -30,2 +30,10 @@ "use strict"; | ||
} | ||
if (message.rawTrailers) { | ||
message.rawTrailers = JSON.parse(message.rawTrailers); | ||
message.trailers = (0, header_utils_1.rawHeadersToObject)(message.rawTrailers); | ||
} | ||
else if (message.rawHeaders && message.body) { // HTTP events with bodies should have trailers | ||
message.rawTrailers = []; | ||
message.trailers = {}; | ||
} | ||
if (message.body !== undefined) { | ||
@@ -187,10 +195,10 @@ // Body is serialized as the raw encoded buffer in base64 | ||
requestInitiated { | ||
id, | ||
protocol, | ||
method, | ||
url, | ||
path, | ||
${this.schema.asOptionalField('InitiatedRequest', 'remoteIpAddress')}, | ||
${this.schema.asOptionalField('InitiatedRequest', 'remotePort')}, | ||
hostname, | ||
id | ||
protocol | ||
method | ||
url | ||
path | ||
${this.schema.asOptionalField('InitiatedRequest', 'remoteIpAddress')} | ||
${this.schema.asOptionalField('InitiatedRequest', 'remotePort')} | ||
hostname | ||
@@ -200,4 +208,4 @@ ${this.schema.typeHasField('InitiatedRequest', 'rawHeaders') | ||
: 'headers'} | ||
timingEvents, | ||
httpVersion, | ||
timingEvents | ||
httpVersion | ||
${this.schema.asOptionalField('InitiatedRequest', 'tags')} | ||
@@ -208,11 +216,11 @@ } | ||
requestReceived { | ||
id, | ||
id | ||
${this.schema.asOptionalField('Request', 'matchedRuleId')} | ||
protocol, | ||
method, | ||
url, | ||
path, | ||
${this.schema.asOptionalField('Request', 'remoteIpAddress')}, | ||
${this.schema.asOptionalField('Request', 'remotePort')}, | ||
hostname, | ||
protocol | ||
method | ||
url | ||
path | ||
${this.schema.asOptionalField('Request', 'remoteIpAddress')} | ||
${this.schema.asOptionalField('Request', 'remotePort')} | ||
hostname | ||
@@ -223,3 +231,5 @@ ${this.schema.typeHasField('Request', 'rawHeaders') | ||
body, | ||
body | ||
${this.schema.asOptionalField('Request', 'rawTrailers')} | ||
${this.schema.asOptionalField('Request', 'timingEvents')} | ||
@@ -232,5 +242,5 @@ ${this.schema.asOptionalField('Request', 'httpVersion')} | ||
responseCompleted { | ||
id, | ||
statusCode, | ||
statusMessage, | ||
id | ||
statusCode | ||
statusMessage | ||
@@ -241,3 +251,5 @@ ${this.schema.typeHasField('Response', 'rawHeaders') | ||
body, | ||
body | ||
${this.schema.asOptionalField('Response', 'rawTrailers')} | ||
${this.schema.asOptionalField('Response', 'timingEvents')} | ||
@@ -249,17 +261,18 @@ ${this.schema.asOptionalField('Response', 'tags')} | ||
webSocketRequest { | ||
id, | ||
matchedRuleId, | ||
protocol, | ||
method, | ||
url, | ||
path, | ||
remoteIpAddress, | ||
remotePort, | ||
hostname, | ||
id | ||
matchedRuleId | ||
protocol | ||
method | ||
url | ||
path | ||
remoteIpAddress | ||
remotePort | ||
hostname | ||
rawHeaders, | ||
body, | ||
rawHeaders | ||
body | ||
${this.schema.asOptionalField('Request', 'rawTrailers')} | ||
timingEvents, | ||
httpVersion, | ||
timingEvents | ||
httpVersion | ||
tags | ||
@@ -270,10 +283,11 @@ } | ||
webSocketAccepted { | ||
id, | ||
statusCode, | ||
statusMessage, | ||
id | ||
statusCode | ||
statusMessage | ||
rawHeaders, | ||
body, | ||
rawHeaders | ||
body | ||
${this.schema.asOptionalField('Response', 'rawTrailers')} | ||
timingEvents, | ||
timingEvents | ||
tags | ||
@@ -284,9 +298,9 @@ } | ||
webSocketMessageReceived { | ||
streamId, | ||
direction, | ||
content, | ||
isBinary, | ||
eventTimestamp, | ||
streamId | ||
direction | ||
content | ||
isBinary | ||
eventTimestamp | ||
timingEvents, | ||
timingEvents | ||
tags | ||
@@ -297,9 +311,9 @@ } | ||
webSocketMessageSent { | ||
streamId, | ||
direction, | ||
content, | ||
isBinary, | ||
eventTimestamp, | ||
streamId | ||
direction | ||
content | ||
isBinary | ||
eventTimestamp | ||
timingEvents, | ||
timingEvents | ||
tags | ||
@@ -310,8 +324,8 @@ } | ||
webSocketClose { | ||
streamId, | ||
streamId | ||
closeCode, | ||
closeReason, | ||
closeCode | ||
closeReason | ||
timingEvents, | ||
timingEvents | ||
tags | ||
@@ -392,4 +406,4 @@ } | ||
${this.schema.asOptionalField('ClientErrorRequest', 'remoteIpAddress')}, | ||
${this.schema.asOptionalField('ClientErrorRequest', 'remotePort')}, | ||
${this.schema.asOptionalField('ClientErrorRequest', 'remoteIpAddress')} | ||
${this.schema.asOptionalField('ClientErrorRequest', 'remotePort')} | ||
} | ||
@@ -408,2 +422,3 @@ response { | ||
body | ||
${this.schema.asOptionalField('Response', 'rawTrailers')} | ||
} | ||
@@ -410,0 +425,0 @@ } |
@@ -5,3 +5,3 @@ /// <reference types="node" /> | ||
import { Readable } from 'stream'; | ||
import { Headers, CompletedRequest, CompletedBody, Explainable, RawHeaders } from "../../types"; | ||
import { Headers, Trailers, CompletedRequest, CompletedBody, Explainable, RawHeaders } from "../../types"; | ||
import { MaybePromise, Replace } from '../../util/type-utils'; | ||
@@ -131,2 +131,10 @@ import { Serializable, ClientServerChannel, SerializedProxyConfig } from "../../serialization/serialization"; | ||
/** | ||
* The replacement HTTP trailers, as an object of string keys and either | ||
* single string or array of string values. Note that there are not all | ||
* header fields are valid as trailers, and there are other requirements | ||
* such as chunked encoding that must be met for trailers to be sent | ||
* successfully. | ||
*/ | ||
trailers?: Trailers; | ||
/** | ||
* A string or buffer, which replaces the response body if set. This will | ||
@@ -180,4 +188,5 @@ * be automatically encoded to match the Content-Encoding defined in your | ||
headers?: Headers | undefined; | ||
trailers?: Trailers | undefined; | ||
readonly type = "simple"; | ||
constructor(status: number, statusMessage?: string | undefined, data?: string | Uint8Array | Buffer | SerializedBuffer | undefined, headers?: Headers | undefined); | ||
constructor(status: number, statusMessage?: string | undefined, data?: string | Uint8Array | Buffer | SerializedBuffer | undefined, headers?: Headers | undefined, trailers?: Trailers | undefined); | ||
explain(): string; | ||
@@ -184,0 +193,0 @@ } |
@@ -31,3 +31,3 @@ "use strict"; | ||
class SimpleHandlerDefinition extends serialization_1.Serializable { | ||
constructor(status, statusMessage, data, headers) { | ||
constructor(status, statusMessage, data, headers, trailers) { | ||
super(); | ||
@@ -38,4 +38,11 @@ this.status = status; | ||
this.headers = headers; | ||
this.trailers = trailers; | ||
this.type = 'simple'; | ||
validateCustomHeaders({}, headers); | ||
validateCustomHeaders({}, trailers); | ||
if (!_.isEmpty(trailers) && headers) { | ||
if (!Object.entries(headers).some(([key, value]) => key.toLowerCase() === 'transfer-encoding' && value === 'chunked')) { | ||
throw new Error("Trailers can only be set when using chunked transfer encoding"); | ||
} | ||
} | ||
} | ||
@@ -46,3 +53,4 @@ explain() { | ||
(this.headers ? `, headers ${JSON.stringify(this.headers)}` : "") + | ||
(this.data ? ` and body "${this.data}"` : ""); | ||
(this.data ? ` and body "${this.data}"` : "") + | ||
(this.trailers ? `then trailers ${JSON.stringify(this.trailers)}` : ""); | ||
} | ||
@@ -49,0 +57,0 @@ } |
@@ -46,2 +46,5 @@ "use strict"; | ||
} | ||
if (this.trailers) { | ||
response.addTrailers(this.trailers); | ||
} | ||
response.end(this.data || ""); | ||
@@ -53,3 +56,5 @@ } | ||
if (result.json !== undefined) { | ||
result.headers = _.assign(result.headers || {}, { 'Content-Type': 'application/json' }); | ||
result.headers = Object.assign(result.headers || {}, { | ||
'Content-Type': 'application/json' | ||
}); | ||
result.body = JSON.stringify(result.json); | ||
@@ -68,2 +73,4 @@ delete result.json; | ||
(0, request_utils_1.writeHead)(response, result.statusCode || result.status || 200, result.statusMessage, result.headers); | ||
if (result.trailers) | ||
response.addTrailers(result.trailers); | ||
response.end(result.rawBody || ""); | ||
@@ -501,2 +508,26 @@ } | ||
}); | ||
// Forward server trailers, if we receive any: | ||
serverRes.on('end', () => { | ||
if (!serverRes.rawTrailers?.length) | ||
return; | ||
const trailersToForward = (0, header_utils_1.pairFlatRawHeaders)(serverRes.rawTrailers) | ||
.filter(([key, value]) => { | ||
if (!(0, header_utils_1.validateHeader)(key, value)) { | ||
console.warn(`Not forwarding invalid trailer: "${key}: ${value}"`); | ||
// Nothing else we can do in this case regardless - setHeaders will | ||
// throw within Node if we try to set this value. | ||
return false; | ||
} | ||
return true; | ||
}); | ||
try { | ||
clientRes.addTrailers((0, request_utils_1.isHttp2)(clientReq) | ||
// HTTP/2 compat doesn't support raw headers here (yet) | ||
? (0, header_utils_1.rawHeadersToObjectPreservingCase)(trailersToForward) | ||
: trailersToForward); | ||
} | ||
catch (e) { | ||
console.warn(`Failed to forward response trailers: ${e}`); | ||
} | ||
}); | ||
let serverStatusCode = serverRes.statusCode; | ||
@@ -690,2 +721,23 @@ let serverStatusMessage = serverRes.statusMessage; | ||
}); | ||
// Forward any request trailers received from the client: | ||
const forwardTrailers = () => { | ||
if (clientReq.rawTrailers?.length) { | ||
if (serverReq.addTrailers) { | ||
serverReq.addTrailers(clientReq.rawTrailers); | ||
} | ||
else { | ||
// See https://github.com/szmarczak/http2-wrapper/issues/103 | ||
console.warn('Not forwarding request trailers - not yet supported for HTTP/2'); | ||
} | ||
} | ||
}; | ||
// This has to be above the pipe setup below, or we end the stream before adding the | ||
// trailers, and they're lost. | ||
if (clientReqBody.readableEnded) { | ||
forwardTrailers(); | ||
} | ||
else { | ||
clientReqBody.once('end', forwardTrailers); | ||
} | ||
// Forward the request body to the upstream server: | ||
if (reqBodyOverride) { | ||
@@ -692,0 +744,0 @@ clientReqBody.resume(); // Dump any remaining real request body |
/// <reference types="node" /> | ||
/// <reference types="node" /> | ||
import { Readable } from "stream"; | ||
import { Headers, CompletedRequest, Method, MockedEndpoint } from "../../types"; | ||
import { Headers, CompletedRequest, Method, MockedEndpoint, Trailers } from "../../types"; | ||
import type { RequestRuleData } from "./request-rule"; | ||
@@ -37,6 +37,6 @@ import { CallbackResponseResult, PassThroughHandlerOptions } from "./request-handler-definitions"; | ||
* Reply to matched requests with a given status code and (optionally) status message, | ||
* body and headers. | ||
* body, headers & trailers. | ||
* | ||
* If one string argument is provided, it's used as the body. If two are | ||
* provided (even if one is empty), then 1st is the status message, and | ||
* provided (even if one is empty) then the 1st is the status message, and | ||
* the 2nd the body. If no headers are provided, only the standard required | ||
@@ -55,4 +55,4 @@ * headers are set, e.g. Date and Transfer-Encoding. | ||
*/ | ||
thenReply(status: number, data?: string | Buffer, headers?: Headers): Promise<MockedEndpoint>; | ||
thenReply(status: number, statusMessage: string, data: string | Buffer, headers?: Headers): Promise<MockedEndpoint>; | ||
thenReply(status: number, data?: string | Buffer, headers?: Headers, trailers?: Trailers): Promise<MockedEndpoint>; | ||
thenReply(status: number, statusMessage: string, data: string | Buffer, headers?: Headers, trailers?: Trailers): Promise<MockedEndpoint>; | ||
/** | ||
@@ -59,0 +59,0 @@ * Reply to matched requests with the given status & JSON and (optionally) |
@@ -54,8 +54,10 @@ "use strict"; | ||
} | ||
thenReply(status, dataOrMessage, dataOrHeaders, headers) { | ||
thenReply(status, dataOrMessage, dataOrHeaders, headersOrTrailers, trailers) { | ||
let data; | ||
let statusMessage; | ||
let headers; | ||
if ((0, lodash_1.isBuffer)(dataOrHeaders) || (0, lodash_1.isString)(dataOrHeaders)) { | ||
data = dataOrHeaders; | ||
statusMessage = dataOrMessage; | ||
headers = headersOrTrailers; | ||
} | ||
@@ -65,6 +67,7 @@ else { | ||
headers = dataOrHeaders; | ||
trailers = headersOrTrailers; | ||
} | ||
const rule = { | ||
...this.buildBaseRuleData(), | ||
handler: new request_handler_definitions_1.SimpleHandlerDefinition(status, statusMessage, data, headers) | ||
handler: new request_handler_definitions_1.SimpleHandlerDefinition(status, statusMessage, data, headers, trailers) | ||
}; | ||
@@ -71,0 +74,0 @@ return this.addRule(rule); |
@@ -55,3 +55,5 @@ "use strict"; | ||
...initiatedRequest, | ||
body: (0, request_utils_1.buildBodyReader)(Buffer.from([]), req.headers) | ||
body: (0, request_utils_1.buildBodyReader)(Buffer.from([]), req.headers), | ||
rawTrailers: [], | ||
trailers: {} | ||
}; | ||
@@ -58,0 +60,0 @@ })); |
@@ -216,3 +216,7 @@ "use strict"; | ||
// nothing else is listening, so we need to catch errors on the socket: | ||
socket.once('error', (e) => console.log('Error on client socket', e)); | ||
socket.once('error', (e) => { | ||
if (options.debug) { | ||
console.log('Error on client socket', e); | ||
} | ||
}); | ||
const connectUrl = req.url || req.headers['host']; | ||
@@ -219,0 +223,0 @@ if (!connectUrl) { |
@@ -453,2 +453,11 @@ "use strict"; | ||
(0, util_1.makePropertyWritable)(req, 'rawHeaders'); | ||
let rawTrailers; | ||
Object.defineProperty(req, 'rawTrailers', { | ||
get: () => rawTrailers, | ||
set: (flatRawTrailers) => { | ||
rawTrailers = flatRawTrailers | ||
? (0, header_utils_1.pairFlatRawHeaders)(flatRawTrailers) | ||
: undefined; | ||
} | ||
}); | ||
return Object.assign(req, { | ||
@@ -458,2 +467,3 @@ id, | ||
rawHeaders, | ||
rawTrailers, | ||
remoteIpAddress: req.socket.remoteAddress, | ||
@@ -753,2 +763,4 @@ remotePort: req.socket.remotePort, | ||
rawHeaders: [['Connection', 'close']], | ||
trailers: {}, | ||
rawTrailers: [], | ||
statusCode: isHeaderOverflow | ||
@@ -755,0 +767,0 @@ ? 431 |
@@ -34,3 +34,7 @@ /// <reference types="node" /> | ||
} | ||
export interface Trailers { | ||
[key: string]: undefined | string | string[]; | ||
} | ||
export declare type RawHeaders = Array<[key: string, value: string]>; | ||
export declare type RawTrailers = RawHeaders; | ||
export interface Request { | ||
@@ -114,2 +118,3 @@ id: string; | ||
body: OngoingBody; | ||
rawTrailers?: RawHeaders; | ||
} | ||
@@ -195,2 +200,4 @@ export interface OngoingBody { | ||
body: CompletedBody; | ||
rawTrailers: RawTrailers; | ||
trailers: Trailers; | ||
} | ||
@@ -212,2 +219,3 @@ export interface TimingEvents { | ||
body: OngoingBody; | ||
getRawTrailers(): RawTrailers; | ||
timingEvents: TimingEvents; | ||
@@ -223,2 +231,4 @@ tags: string[]; | ||
body: CompletedBody; | ||
rawTrailers: RawTrailers; | ||
trailers: Trailers; | ||
timingEvents: TimingEvents; | ||
@@ -225,0 +235,0 @@ tags: string[]; |
@@ -263,5 +263,31 @@ "use strict"; | ||
const requestData = buildInitiatedRequest(request); | ||
return { ...requestData, body }; | ||
return { | ||
...requestData, | ||
body, | ||
rawTrailers: request.rawTrailers ?? [], | ||
trailers: (0, header_utils_1.rawHeadersToObject)(request.rawTrailers ?? []) | ||
}; | ||
} | ||
exports.waitForCompletedRequest = waitForCompletedRequest; | ||
/** | ||
* Parse the accepted format of the headers argument for writeHead and addTrailers | ||
* into a single consistent paired-tuple format. | ||
*/ | ||
const getHeaderPairsFromArgument = (headersArg) => { | ||
// Two legal formats of header args (flat & object), one unofficial (tuple array) | ||
if (Array.isArray(headersArg)) { | ||
if (!Array.isArray(headersArg[0])) { | ||
// Flat -> Raw tuples | ||
return (0, header_utils_1.pairFlatRawHeaders)(headersArg); | ||
} | ||
else { | ||
// Already raw tuples, cheeky | ||
return headersArg; | ||
} | ||
} | ||
else { | ||
// Headers object -> raw tuples | ||
return (0, header_utils_1.objectHeadersToRaw)(headersArg ?? {}); | ||
} | ||
}; | ||
function trackResponse(response, timingEvents, tags, options) { | ||
@@ -276,2 +302,3 @@ let trackedResponse = response; | ||
const originalEnd = trackedResponse.end; | ||
const originalAddTrailers = trackedResponse.addTrailers; | ||
const originalGetHeaders = trackedResponse.getHeaders; | ||
@@ -296,17 +323,6 @@ let writtenHeaders; | ||
} | ||
// Two legal formats of header args (flat & object), one unofficial (tuple array) | ||
if (Array.isArray(headersArg)) { | ||
if (!Array.isArray(headersArg[0])) { | ||
// Flat -> Raw tuples | ||
writtenHeaders = (0, header_utils_1.pairFlatRawHeaders)(headersArg); | ||
} | ||
else { | ||
// Already raw tuples, cheeky | ||
writtenHeaders = headersArg; | ||
} | ||
writtenHeaders = getHeaderPairsFromArgument(headersArg); | ||
if (isHttp2(trackedResponse)) { | ||
writtenHeaders.unshift([':status', args[0].toString()]); | ||
} | ||
else { | ||
// Headers object -> raw tuples | ||
writtenHeaders = (0, header_utils_1.objectHeadersToRaw)(headersArg ?? {}); | ||
} | ||
// Headers might also have been set with setHeader before. They'll be combined, with headers | ||
@@ -334,2 +350,9 @@ // here taking precendence. We simulate this by pulling in all values from getHeaders() and | ||
}; | ||
let writtenTrailers; | ||
trackedResponse.getRawTrailers = () => writtenTrailers ?? []; | ||
trackedResponse.addTrailers = function (...args) { | ||
const trailersArg = args[0]; | ||
writtenTrailers = getHeaderPairsFromArgument(trailersArg); | ||
return originalAddTrailers.apply(this, args); | ||
}; | ||
const trackingWrite = function (...args) { | ||
@@ -377,3 +400,5 @@ trackingStream.write.apply(trackingStream, args); | ||
rawHeaders: response.getRawHeaders(), | ||
body: body | ||
body: body, | ||
rawTrailers: response.getRawTrailers(), | ||
trailers: (0, header_utils_1.rawHeadersToObject)(response.getRawTrailers()) | ||
}).valueOf(); | ||
@@ -472,3 +497,5 @@ if (!(response instanceof http2.Http2ServerResponse)) { | ||
headers, | ||
body | ||
body, | ||
rawTrailers: [], | ||
trailers: {} | ||
}; | ||
@@ -475,0 +502,0 @@ } |
{ | ||
"name": "mockttp", | ||
"version": "3.10.2", | ||
"version": "3.11.0", | ||
"description": "Mock HTTP server for testing HTTP clients and stubbing webservices", | ||
@@ -178,3 +178,3 @@ "exports": { | ||
"cross-fetch": "^3.1.5", | ||
"destroyable-server": "^1.0.0", | ||
"destroyable-server": "^1.0.2", | ||
"express": "^4.14.0", | ||
@@ -181,0 +181,0 @@ "graphql": "^14.0.2 || ^15.5", |
@@ -162,2 +162,3 @@ import gql from "graphql-tag"; | ||
body: Buffer! | ||
rawTrailers: Json! | ||
} | ||
@@ -199,2 +200,3 @@ | ||
body: Buffer! | ||
rawTrailers: Json! | ||
} | ||
@@ -201,0 +203,0 @@ |
@@ -41,2 +41,10 @@ import _ = require('lodash'); | ||
if (message.rawTrailers) { | ||
message.rawTrailers = JSON.parse(message.rawTrailers); | ||
message.trailers = rawHeadersToObject(message.rawTrailers); | ||
} else if (message.rawHeaders && message.body) { // HTTP events with bodies should have trailers | ||
message.rawTrailers = []; | ||
message.trailers = {}; | ||
} | ||
if (message.body !== undefined) { | ||
@@ -222,10 +230,10 @@ // Body is serialized as the raw encoded buffer in base64 | ||
requestInitiated { | ||
id, | ||
protocol, | ||
method, | ||
url, | ||
path, | ||
${this.schema.asOptionalField('InitiatedRequest', 'remoteIpAddress')}, | ||
${this.schema.asOptionalField('InitiatedRequest', 'remotePort')}, | ||
hostname, | ||
id | ||
protocol | ||
method | ||
url | ||
path | ||
${this.schema.asOptionalField('InitiatedRequest', 'remoteIpAddress')} | ||
${this.schema.asOptionalField('InitiatedRequest', 'remotePort')} | ||
hostname | ||
@@ -236,4 +244,4 @@ ${this.schema.typeHasField('InitiatedRequest', 'rawHeaders') | ||
} | ||
timingEvents, | ||
httpVersion, | ||
timingEvents | ||
httpVersion | ||
${this.schema.asOptionalField('InitiatedRequest', 'tags')} | ||
@@ -244,11 +252,11 @@ } | ||
requestReceived { | ||
id, | ||
id | ||
${this.schema.asOptionalField('Request', 'matchedRuleId')} | ||
protocol, | ||
method, | ||
url, | ||
path, | ||
${this.schema.asOptionalField('Request', 'remoteIpAddress')}, | ||
${this.schema.asOptionalField('Request', 'remotePort')}, | ||
hostname, | ||
protocol | ||
method | ||
url | ||
path | ||
${this.schema.asOptionalField('Request', 'remoteIpAddress')} | ||
${this.schema.asOptionalField('Request', 'remotePort')} | ||
hostname | ||
@@ -260,3 +268,5 @@ ${this.schema.typeHasField('Request', 'rawHeaders') | ||
body, | ||
body | ||
${this.schema.asOptionalField('Request', 'rawTrailers')} | ||
${this.schema.asOptionalField('Request', 'timingEvents')} | ||
@@ -269,5 +279,5 @@ ${this.schema.asOptionalField('Request', 'httpVersion')} | ||
responseCompleted { | ||
id, | ||
statusCode, | ||
statusMessage, | ||
id | ||
statusCode | ||
statusMessage | ||
@@ -279,3 +289,5 @@ ${this.schema.typeHasField('Response', 'rawHeaders') | ||
body, | ||
body | ||
${this.schema.asOptionalField('Response', 'rawTrailers')} | ||
${this.schema.asOptionalField('Response', 'timingEvents')} | ||
@@ -287,17 +299,18 @@ ${this.schema.asOptionalField('Response', 'tags')} | ||
webSocketRequest { | ||
id, | ||
matchedRuleId, | ||
protocol, | ||
method, | ||
url, | ||
path, | ||
remoteIpAddress, | ||
remotePort, | ||
hostname, | ||
id | ||
matchedRuleId | ||
protocol | ||
method | ||
url | ||
path | ||
remoteIpAddress | ||
remotePort | ||
hostname | ||
rawHeaders, | ||
body, | ||
rawHeaders | ||
body | ||
${this.schema.asOptionalField('Request', 'rawTrailers')} | ||
timingEvents, | ||
httpVersion, | ||
timingEvents | ||
httpVersion | ||
tags | ||
@@ -308,10 +321,11 @@ } | ||
webSocketAccepted { | ||
id, | ||
statusCode, | ||
statusMessage, | ||
id | ||
statusCode | ||
statusMessage | ||
rawHeaders, | ||
body, | ||
rawHeaders | ||
body | ||
${this.schema.asOptionalField('Response', 'rawTrailers')} | ||
timingEvents, | ||
timingEvents | ||
tags | ||
@@ -322,9 +336,9 @@ } | ||
webSocketMessageReceived { | ||
streamId, | ||
direction, | ||
content, | ||
isBinary, | ||
eventTimestamp, | ||
streamId | ||
direction | ||
content | ||
isBinary | ||
eventTimestamp | ||
timingEvents, | ||
timingEvents | ||
tags | ||
@@ -335,9 +349,9 @@ } | ||
webSocketMessageSent { | ||
streamId, | ||
direction, | ||
content, | ||
isBinary, | ||
eventTimestamp, | ||
streamId | ||
direction | ||
content | ||
isBinary | ||
eventTimestamp | ||
timingEvents, | ||
timingEvents | ||
tags | ||
@@ -348,8 +362,8 @@ } | ||
webSocketClose { | ||
streamId, | ||
streamId | ||
closeCode, | ||
closeReason, | ||
closeCode | ||
closeReason | ||
timingEvents, | ||
timingEvents | ||
tags | ||
@@ -432,4 +446,4 @@ } | ||
${this.schema.asOptionalField('ClientErrorRequest', 'remoteIpAddress')}, | ||
${this.schema.asOptionalField('ClientErrorRequest', 'remotePort')}, | ||
${this.schema.asOptionalField('ClientErrorRequest', 'remoteIpAddress')} | ||
${this.schema.asOptionalField('ClientErrorRequest', 'remotePort')} | ||
} | ||
@@ -449,2 +463,3 @@ response { | ||
body | ||
${this.schema.asOptionalField('Response', 'rawTrailers')} | ||
} | ||
@@ -451,0 +466,0 @@ } |
@@ -10,2 +10,3 @@ import _ = require('lodash'); | ||
Headers, | ||
Trailers, | ||
CompletedRequest, | ||
@@ -185,2 +186,11 @@ CompletedBody, | ||
/** | ||
* The replacement HTTP trailers, as an object of string keys and either | ||
* single string or array of string values. Note that there are not all | ||
* header fields are valid as trailers, and there are other requirements | ||
* such as chunked encoding that must be met for trailers to be sent | ||
* successfully. | ||
*/ | ||
trailers?: Trailers; | ||
/** | ||
* A string or buffer, which replaces the response body if set. This will | ||
@@ -266,3 +276,4 @@ * be automatically encoded to match the Content-Encoding defined in your | ||
public data?: string | Uint8Array | Buffer | SerializedBuffer, | ||
public headers?: Headers | ||
public headers?: Headers, | ||
public trailers?: Trailers | ||
) { | ||
@@ -272,2 +283,11 @@ super(); | ||
validateCustomHeaders({}, headers); | ||
validateCustomHeaders({}, trailers); | ||
if (!_.isEmpty(trailers) && headers) { | ||
if (!Object.entries(headers!).some(([key, value]) => | ||
key.toLowerCase() === 'transfer-encoding' && value === 'chunked' | ||
)) { | ||
throw new Error("Trailers can only be set when using chunked transfer encoding"); | ||
} | ||
} | ||
} | ||
@@ -279,3 +299,4 @@ | ||
(this.headers ? `, headers ${JSON.stringify(this.headers)}` : "") + | ||
(this.data ? ` and body "${this.data}"` : ""); | ||
(this.data ? ` and body "${this.data}"` : "") + | ||
(this.trailers ? `then trailers ${JSON.stringify(this.trailers)}` : ""); | ||
} | ||
@@ -282,0 +303,0 @@ } |
@@ -165,2 +165,6 @@ import _ = require('lodash'); | ||
if (this.trailers) { | ||
response.addTrailers(this.trailers); | ||
} | ||
response.end(this.data || ""); | ||
@@ -170,5 +174,10 @@ } | ||
async function writeResponseFromCallback(result: CallbackResponseMessageResult, response: OngoingResponse) { | ||
async function writeResponseFromCallback( | ||
result: CallbackResponseMessageResult, | ||
response: OngoingResponse | ||
) { | ||
if (result.json !== undefined) { | ||
result.headers = _.assign(result.headers || {}, { 'Content-Type': 'application/json' }); | ||
result.headers = Object.assign(result.headers || {}, { | ||
'Content-Type': 'application/json' | ||
}); | ||
result.body = JSON.stringify(result.json); | ||
@@ -198,2 +207,5 @@ delete result.json; | ||
); | ||
if (result.trailers) response.addTrailers(result.trailers); | ||
response.end(result.rawBody || ""); | ||
@@ -761,2 +773,29 @@ } | ||
// Forward server trailers, if we receive any: | ||
serverRes.on('end', () => { | ||
if (!serverRes.rawTrailers?.length) return; | ||
const trailersToForward = pairFlatRawHeaders(serverRes.rawTrailers) | ||
.filter(([key, value]) => { | ||
if (!validateHeader(key, value)) { | ||
console.warn(`Not forwarding invalid trailer: "${key}: ${value}"`); | ||
// Nothing else we can do in this case regardless - setHeaders will | ||
// throw within Node if we try to set this value. | ||
return false; | ||
} | ||
return true; | ||
}); | ||
try { | ||
clientRes.addTrailers( | ||
isHttp2(clientReq) | ||
// HTTP/2 compat doesn't support raw headers here (yet) | ||
? rawHeadersToObjectPreservingCase(trailersToForward) | ||
: trailersToForward | ||
); | ||
} catch (e) { | ||
console.warn(`Failed to forward response trailers: ${e}`); | ||
} | ||
}); | ||
let serverStatusCode = serverRes.statusCode!; | ||
@@ -1001,2 +1040,22 @@ let serverStatusMessage = serverRes.statusMessage | ||
// Forward any request trailers received from the client: | ||
const forwardTrailers = () => { | ||
if (clientReq.rawTrailers?.length) { | ||
if (serverReq.addTrailers) { | ||
serverReq.addTrailers(clientReq.rawTrailers); | ||
} else { | ||
// See https://github.com/szmarczak/http2-wrapper/issues/103 | ||
console.warn('Not forwarding request trailers - not yet supported for HTTP/2'); | ||
} | ||
} | ||
}; | ||
// This has to be above the pipe setup below, or we end the stream before adding the | ||
// trailers, and they're lost. | ||
if (clientReqBody.readableEnded) { | ||
forwardTrailers(); | ||
} else { | ||
clientReqBody.once('end', forwardTrailers); | ||
} | ||
// Forward the request body to the upstream server: | ||
if (reqBodyOverride) { | ||
@@ -1003,0 +1062,0 @@ clientReqBody.resume(); // Dump any remaining real request body |
import { merge, isString, isBuffer } from "lodash"; | ||
import { Readable } from "stream"; | ||
import { Headers, CompletedRequest, Method, MockedEndpoint } from "../../types"; | ||
import { Headers, CompletedRequest, Method, MockedEndpoint, Trailers } from "../../types"; | ||
import type { RequestRuleData } from "./request-rule"; | ||
@@ -90,6 +90,6 @@ | ||
* Reply to matched requests with a given status code and (optionally) status message, | ||
* body and headers. | ||
* body, headers & trailers. | ||
* | ||
* If one string argument is provided, it's used as the body. If two are | ||
* provided (even if one is empty), then 1st is the status message, and | ||
* provided (even if one is empty) then the 1st is the status message, and | ||
* the 2nd the body. If no headers are provided, only the standard required | ||
@@ -108,8 +108,14 @@ * headers are set, e.g. Date and Transfer-Encoding. | ||
*/ | ||
thenReply(status: number, data?: string | Buffer, headers?: Headers): Promise<MockedEndpoint>; | ||
thenReply( | ||
status: number, | ||
data?: string | Buffer, | ||
headers?: Headers, | ||
trailers?: Trailers | ||
): Promise<MockedEndpoint>; | ||
thenReply( | ||
status: number, | ||
statusMessage: string, | ||
data: string | Buffer, | ||
headers?: Headers | ||
headers?: Headers, | ||
trailers?: Trailers | ||
): Promise<MockedEndpoint> | ||
@@ -120,12 +126,17 @@ thenReply( | ||
dataOrHeaders?: string | Buffer | Headers, | ||
headers?: Headers | ||
headersOrTrailers?: Headers | Trailers, | ||
trailers?: Trailers | ||
): Promise<MockedEndpoint> { | ||
let data: string | Buffer | undefined; | ||
let statusMessage: string | undefined; | ||
let headers: Headers | undefined; | ||
if (isBuffer(dataOrHeaders) || isString(dataOrHeaders)) { | ||
data = dataOrHeaders as (Buffer | string); | ||
statusMessage = dataOrMessage as string; | ||
headers = headersOrTrailers as Headers; | ||
} else { | ||
data = dataOrMessage as string | Buffer | undefined; | ||
headers = dataOrHeaders as Headers | undefined; | ||
trailers = headersOrTrailers as Trailers | undefined; | ||
} | ||
@@ -135,3 +146,9 @@ | ||
...this.buildBaseRuleData(), | ||
handler: new SimpleHandlerDefinition(status, statusMessage, data, headers) | ||
handler: new SimpleHandlerDefinition( | ||
status, | ||
statusMessage, | ||
data, | ||
headers, | ||
trailers | ||
) | ||
}; | ||
@@ -138,0 +155,0 @@ |
@@ -99,3 +99,5 @@ import * as _ from 'lodash'; | ||
...initiatedRequest, | ||
body: buildBodyReader(Buffer.from([]), req.headers) | ||
body: buildBodyReader(Buffer.from([]), req.headers), | ||
rawTrailers: [], | ||
trailers: {} | ||
}; | ||
@@ -102,0 +104,0 @@ }) |
@@ -276,3 +276,7 @@ import _ = require('lodash'); | ||
// nothing else is listening, so we need to catch errors on the socket: | ||
socket.once('error', (e) => console.log('Error on client socket', e)); | ||
socket.once('error', (e) => { | ||
if (options.debug) { | ||
console.log('Error on client socket', e); | ||
} | ||
}); | ||
@@ -279,0 +283,0 @@ const connectUrl = req.url || req.headers['host']; |
@@ -29,3 +29,4 @@ import _ = require("lodash"); | ||
TlsPassthroughEvent, | ||
RuleEvent | ||
RuleEvent, | ||
RawTrailers | ||
} from "../types"; | ||
@@ -618,2 +619,12 @@ import { DestroyableServer } from "destroyable-server"; | ||
let rawTrailers: RawTrailers | undefined; | ||
Object.defineProperty(req, 'rawTrailers', { | ||
get: () => rawTrailers, | ||
set: (flatRawTrailers) => { | ||
rawTrailers = flatRawTrailers | ||
? pairFlatRawHeaders(flatRawTrailers) | ||
: undefined; | ||
} | ||
}); | ||
return Object.assign(req, { | ||
@@ -623,2 +634,3 @@ id, | ||
rawHeaders, | ||
rawTrailers, // Just makes the type happy - really managed by property above | ||
remoteIpAddress: req.socket.remoteAddress, | ||
@@ -973,2 +985,4 @@ remotePort: req.socket.remotePort, | ||
rawHeaders: [['Connection', 'close']], | ||
trailers: {}, | ||
rawTrailers: [], | ||
statusCode: | ||
@@ -975,0 +989,0 @@ isHeaderOverflow |
@@ -39,3 +39,9 @@ import stream = require('stream'); | ||
export interface Trailers { | ||
// 0+ of any trailer | ||
[key: string]: undefined | string | string[]; | ||
} | ||
export type RawHeaders = Array<[key: string, value: string]>; | ||
export type RawTrailers = RawHeaders; // Just a convenient alias | ||
@@ -146,2 +152,3 @@ export interface Request { | ||
body: OngoingBody; | ||
rawTrailers?: RawHeaders; | ||
} | ||
@@ -229,2 +236,4 @@ | ||
body: CompletedBody; | ||
rawTrailers: RawTrailers; | ||
trailers: Trailers; | ||
} | ||
@@ -254,2 +263,3 @@ | ||
body: OngoingBody; | ||
getRawTrailers(): RawTrailers; | ||
timingEvents: TimingEvents; | ||
@@ -266,2 +276,4 @@ tags: string[]; | ||
body: CompletedBody; | ||
rawTrailers: RawTrailers; | ||
trailers: Trailers; | ||
timingEvents: TimingEvents; | ||
@@ -268,0 +280,0 @@ tags: string[]; |
@@ -347,5 +347,30 @@ import * as _ from 'lodash'; | ||
const requestData = buildInitiatedRequest(request); | ||
return { ...requestData, body }; | ||
return { | ||
...requestData, | ||
body, | ||
rawTrailers: request.rawTrailers ?? [], | ||
trailers: rawHeadersToObject(request.rawTrailers ?? []) | ||
}; | ||
} | ||
/** | ||
* Parse the accepted format of the headers argument for writeHead and addTrailers | ||
* into a single consistent paired-tuple format. | ||
*/ | ||
const getHeaderPairsFromArgument = (headersArg: any) => { | ||
// Two legal formats of header args (flat & object), one unofficial (tuple array) | ||
if (Array.isArray(headersArg)) { | ||
if (!Array.isArray(headersArg[0])) { | ||
// Flat -> Raw tuples | ||
return pairFlatRawHeaders(headersArg); | ||
} else { | ||
// Already raw tuples, cheeky | ||
return headersArg; | ||
} | ||
} else { | ||
// Headers object -> raw tuples | ||
return objectHeadersToRaw(headersArg ?? {}); | ||
} | ||
}; | ||
export function trackResponse( | ||
@@ -369,2 +394,3 @@ response: http.ServerResponse, | ||
const originalEnd = trackedResponse.end; | ||
const originalAddTrailers = trackedResponse.addTrailers; | ||
const originalGetHeaders = trackedResponse.getHeaders; | ||
@@ -393,14 +419,6 @@ | ||
// Two legal formats of header args (flat & object), one unofficial (tuple array) | ||
if (Array.isArray(headersArg)) { | ||
if (!Array.isArray(headersArg[0])) { | ||
// Flat -> Raw tuples | ||
writtenHeaders = pairFlatRawHeaders(headersArg); | ||
} else { | ||
// Already raw tuples, cheeky | ||
writtenHeaders = headersArg; | ||
} | ||
} else { | ||
// Headers object -> raw tuples | ||
writtenHeaders = objectHeadersToRaw(headersArg ?? {}); | ||
writtenHeaders = getHeaderPairsFromArgument(headersArg); | ||
if (isHttp2(trackedResponse)) { | ||
writtenHeaders.unshift([':status', args[0].toString()]); | ||
} | ||
@@ -429,4 +447,13 @@ | ||
return originalWriteHeader.apply(this, args); | ||
} | ||
}; | ||
let writtenTrailers: RawHeaders | undefined; | ||
trackedResponse.getRawTrailers = () => writtenTrailers ?? []; | ||
trackedResponse.addTrailers = function (this: typeof trackedResponse, ...args: any) { | ||
const trailersArg = args[0]; | ||
writtenTrailers = getHeaderPairsFromArgument(trailersArg); | ||
return originalAddTrailers.apply(this, args); | ||
}; | ||
const trackingWrite = function (this: typeof trackedResponse, ...args: any) { | ||
@@ -487,5 +514,10 @@ trackingStream.write.apply(trackingStream, args); | ||
statusMessage: '', | ||
headers: response.getHeaders(), | ||
rawHeaders: response.getRawHeaders(), | ||
body: body | ||
body: body, | ||
rawTrailers: response.getRawTrailers(), | ||
trailers: rawHeadersToObject(response.getRawTrailers()) | ||
}).valueOf(); | ||
@@ -612,4 +644,6 @@ | ||
headers, | ||
body | ||
body, | ||
rawTrailers: [], | ||
trailers: {} | ||
}; | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1527517
26086
Updateddestroyable-server@^1.0.2