From d31283f4b2db043f97548a4af5a626208bd28f3e Mon Sep 17 00:00:00 2001 From: OnkelTem Date: Wed, 11 May 2022 17:01:39 +0300 Subject: [PATCH 1/3] Draft implementation of server.on(handle-error, ...) --- src/client/mockttp-admin-request-builder.ts | 40 ++++++++++++++- src/mockttp.ts | 22 ++++++++- src/server/mockttp-server.ts | 54 ++++++++++++--------- 3 files changed, 89 insertions(+), 27 deletions(-) diff --git a/src/client/mockttp-admin-request-builder.ts b/src/client/mockttp-admin-request-builder.ts index a79fcbf06..2e8448a69 100644 --- a/src/client/mockttp-admin-request-builder.ts +++ b/src/client/mockttp-admin-request-builder.ts @@ -303,6 +303,44 @@ export class MockttpAdminRequestBuilder { body } } + }`, + // TODO: This is just a copy of the previous client-error section. Needs to be finished. + 'handle-error': gql`subscription OnError { + failedRequest { + errorCode + request { + id + timingEvents + tags + protocol + httpVersion + method + url + path + + ${this.schema.typeHasField('ClientErrorRequest', 'rawHeaders') + ? 'rawHeaders' + : 'headers' + } + + ${this.schema.asOptionalField('ClientErrorRequest', 'remoteIpAddress')}, + ${this.schema.asOptionalField('ClientErrorRequest', 'remotePort')}, + } + response { + id + timingEvents + tags + statusCode + statusMessage + + ${this.schema.typeHasField('Response', 'rawHeaders') + ? 'rawHeaders' + : 'headers' + } + + body + } + } }` }[event]; @@ -367,4 +405,4 @@ export class MockttpAdminRequestBuilder { return mockedEndpoint; } -} \ No newline at end of file +} diff --git a/src/mockttp.ts b/src/mockttp.ts index 10be99b83..47ccb7234 100644 --- a/src/mockttp.ts +++ b/src/mockttp.ts @@ -415,6 +415,23 @@ export interface Mockttp { */ on(event: 'client-error', callback: (error: ClientError) => void): Promise; + /** + * Subscribe to hear about requests that fail before successfully sending their + * initial parameters (the request line & headers). This will fire for requests + * that drop connections early, send invalid or too-long headers, or aren't + * correctly parseable in some form. + * + * This is only useful in some niche use cases, such as logging all requests + * seen by the server, independently of the rules defined. + * + * The callback will be called asynchronously from request handling. This function + * returns a promise, and the callback is not guaranteed to be registered until + * the promise is resolved. + * + * @category Events + */ + on(event: 'handle-error', callback: (error: ClientError) => void): Promise; + /** * Adds the given rules to the server. * @@ -576,7 +593,8 @@ export type SubscribableEvent = | 'response' | 'abort' | 'tls-client-error' - | 'client-error'; + | 'client-error' + | 'handle-error'; /** * @hidden @@ -677,4 +695,4 @@ export abstract class AbstractMockttp { return new WebSocketRuleBuilder(this.addWebSocketRule); } -} \ No newline at end of file +} diff --git a/src/server/mockttp-server.ts b/src/server/mockttp-server.ts index d79ba1f9d..c6d26912e 100644 --- a/src/server/mockttp-server.ts +++ b/src/server/mockttp-server.ts @@ -277,6 +277,7 @@ export class MockttpServer extends AbstractMockttp implements Mockttp { public on(event: 'tls-client-error', callback: (req: TlsRequest) => void): Promise; public on(event: 'tlsClientError', callback: (req: TlsRequest) => void): Promise; public on(event: 'client-error', callback: (error: ClientError) => void): Promise; + public on(event: 'handle-error', callback: (error: ClientError) => void): Promise; public on(event: string, callback: (...args: any[]) => void): Promise { this.eventEmitter.on(event, callback); return Promise.resolve(); @@ -464,32 +465,37 @@ export class MockttpServer extends AbstractMockttp implements Mockttp { } result = result || 'responded'; } catch (e) { - if (e instanceof AbortError) { - abort(); - - if (this.debug) { - console.error("Failed to handle request due to abort:", e); - } - } else { - console.error("Failed to handle request:", - this.debug - ? e - : (isErrorLike(e) && e.message) || e - ); + if (this.eventEmitter.listeners('handle-error').length > 0) { + this.eventEmitter.emit('handle-error', e); + } + else { + if (e instanceof AbortError) { + abort(); - // Do whatever we can to tell the client we broke - try { - response.writeHead( - (isErrorLike(e) && e.statusCode) || 500, - (isErrorLike(e) && e.statusMessage) || 'Server error' + if (this.debug) { + console.error("Failed to handle request due to abort:", e); + } + } else { + console.error("Failed to handle request:", + this.debug + ? e + : (isErrorLike(e) && e.message) || e ); - } catch (e) {} - try { - response.end((isErrorLike(e) && e.toString()) || e); - result = result || 'responded'; - } catch (e) { - abort(); + // Do whatever we can to tell the client we broke + try { + response.writeHead( + (isErrorLike(e) && e.statusCode) || 500, + (isErrorLike(e) && e.statusMessage) || 'Server error' + ); + } catch (e) {} + + try { + response.end((isErrorLike(e) && e.toString()) || e); + result = result || 'responded'; + } catch (e) { + abort(); + } } } } @@ -818,4 +824,4 @@ ${await this.suggestRule(request)}` response: 'aborted' // These h2 errors get no app-level response, just a shutdown. }); } -} \ No newline at end of file +} From ae9a37ecdb7529f2dba04fc1a5ebe4d40657c6b7 Mon Sep 17 00:00:00 2001 From: OnkelTem Date: Wed, 11 May 2022 18:05:09 +0300 Subject: [PATCH 2/3] Switched the argument of handle-error to ErrorLike from src/util --- src/mockttp.ts | 3 ++- src/server/mockttp-server.ts | 4 ++-- src/types.ts | 2 +- src/util/error.ts | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/mockttp.ts b/src/mockttp.ts index 47ccb7234..ad9cb9304 100644 --- a/src/mockttp.ts +++ b/src/mockttp.ts @@ -19,6 +19,7 @@ import { } from "./types"; import type { RequestRuleData } from "./rules/requests/request-rule"; import type { WebSocketRuleData } from "./rules/websockets/websocket-rule"; +import { ErrorLike } from "./util/error"; export type PortRange = { startPort: number, endPort: number }; @@ -430,7 +431,7 @@ export interface Mockttp { * * @category Events */ - on(event: 'handle-error', callback: (error: ClientError) => void): Promise; + on(event: 'handle-error', callback: (error: ErrorLike) => void): Promise; /** * Adds the given rules to the server. diff --git a/src/server/mockttp-server.ts b/src/server/mockttp-server.ts index c6d26912e..f89e43d62 100644 --- a/src/server/mockttp-server.ts +++ b/src/server/mockttp-server.ts @@ -30,7 +30,7 @@ import { ServerMockedEndpoint } from "./mocked-endpoint"; import { createComboServer } from "./http-combo-server"; import { filter } from "../util/promise"; import { Mutable } from "../util/type-utils"; -import { isErrorLike } from "../util/error"; +import { ErrorLike, isErrorLike } from "../util/error"; import { makePropertyWritable } from "../util/util"; import { @@ -277,7 +277,7 @@ export class MockttpServer extends AbstractMockttp implements Mockttp { public on(event: 'tls-client-error', callback: (req: TlsRequest) => void): Promise; public on(event: 'tlsClientError', callback: (req: TlsRequest) => void): Promise; public on(event: 'client-error', callback: (error: ClientError) => void): Promise; - public on(event: 'handle-error', callback: (error: ClientError) => void): Promise; + public on(event: 'handle-error', callback: (error: ErrorLike) => void): Promise; public on(event: string, callback: (...args: any[]) => void): Promise { this.eventEmitter.on(event, callback); return Promise.resolve(); diff --git a/src/types.ts b/src/types.ts index 3153cdb2b..f111874cd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -256,4 +256,4 @@ export interface ProxyEnvConfig { // A slightly weird one: this is necessary because we export types that inherit from EventEmitter, // so the docs include EventEmitter's methods, which @link to this type, that's otherwise not // defined in this module. Reexporting the values avoids warnings for that. -export type defaultMaxListeners = typeof EventEmitter.defaultMaxListeners; \ No newline at end of file +export type defaultMaxListeners = typeof EventEmitter.defaultMaxListeners; diff --git a/src/util/error.ts b/src/util/error.ts index d7a4c5527..aaa1b5ce7 100644 --- a/src/util/error.ts +++ b/src/util/error.ts @@ -1,3 +1,4 @@ +// A server error export type ErrorLike = Partial & { // Various properties we might want to look for on errors: code?: string; @@ -15,4 +16,4 @@ export function isErrorLike(error: any): error is ErrorLike { error.code || error.stack ) -} \ No newline at end of file +} From 317581e56607b658855b74650e04040a052b900e Mon Sep 17 00:00:00 2001 From: OnkelTem Date: Fri, 13 May 2022 00:40:47 +0300 Subject: [PATCH 3/3] Renamed 'handle-error' to 'request-handler-error'; reused ErrorLike as RequestHandlerError type; added gql schema and builder; no tests yet --- src/admin/mockttp-admin-model.ts | 9 +++- src/admin/mockttp-schema.ts | 10 ++++- src/client/mockttp-admin-request-builder.ts | 43 +++---------------- src/main.ts | 4 +- src/mockttp.ts | 16 +++---- .../requests/request-handler-definitions.ts | 2 +- src/rules/requests/request-handlers.ts | 2 +- src/server/mockttp-server.ts | 11 ++--- src/types.ts | 6 +++ src/util/error.ts | 1 - 10 files changed, 47 insertions(+), 57 deletions(-) diff --git a/src/admin/mockttp-admin-model.ts b/src/admin/mockttp-admin-model.ts index 4fa705495..3e2f3f9a1 100644 --- a/src/admin/mockttp-admin-model.ts +++ b/src/admin/mockttp-admin-model.ts @@ -25,6 +25,7 @@ const RESPONSE_COMPLETED_TOPIC = 'response-completed'; const REQUEST_ABORTED_TOPIC = 'request-aborted'; const TLS_CLIENT_ERROR_TOPIC = 'tls-client-error'; const CLIENT_ERROR_TOPIC = 'client-error'; +const REQUEST_HANDLER_ERROR_TOPIC = 'request-handler-error'; async function buildMockedEndpointData(endpoint: ServerMockedEndpoint): Promise { return { @@ -82,6 +83,12 @@ export function buildAdminServerModel( }) }); + mockServer.on('request-handler-error', (error) => { + pubsub.publish(REQUEST_HANDLER_ERROR_TOPIC, { + requestHandlerError: error + }) + }); + return { Query: { mockedEndpoints: async (): Promise => { @@ -181,4 +188,4 @@ export function buildAdminServerModel( } } }; -} \ No newline at end of file +} diff --git a/src/admin/mockttp-schema.ts b/src/admin/mockttp-schema.ts index d288c4395..f729da003 100644 --- a/src/admin/mockttp-schema.ts +++ b/src/admin/mockttp-schema.ts @@ -81,6 +81,14 @@ export const MockttpSchema = gql` remotePort: Int! } + type RequestHandlerError { + code: String + cmd: String + signal: String + statusCode: Int + statusMessage: String + } + type InitiatedRequest { id: ID! timingEvents: Json! @@ -133,4 +141,4 @@ export const MockttpSchema = gql` rawHeaders: Json! body: Buffer! } -`; \ No newline at end of file +`; diff --git a/src/client/mockttp-admin-request-builder.ts b/src/client/mockttp-admin-request-builder.ts index 2e8448a69..3a4b1f443 100644 --- a/src/client/mockttp-admin-request-builder.ts +++ b/src/client/mockttp-admin-request-builder.ts @@ -304,42 +304,13 @@ export class MockttpAdminRequestBuilder { } } }`, - // TODO: This is just a copy of the previous client-error section. Needs to be finished. - 'handle-error': gql`subscription OnError { - failedRequest { - errorCode - request { - id - timingEvents - tags - protocol - httpVersion - method - url - path - - ${this.schema.typeHasField('ClientErrorRequest', 'rawHeaders') - ? 'rawHeaders' - : 'headers' - } - - ${this.schema.asOptionalField('ClientErrorRequest', 'remoteIpAddress')}, - ${this.schema.asOptionalField('ClientErrorRequest', 'remotePort')}, - } - response { - id - timingEvents - tags - statusCode - statusMessage - - ${this.schema.typeHasField('Response', 'rawHeaders') - ? 'rawHeaders' - : 'headers' - } - - body - } + 'request-handler-error': gql`subscription OnRequestHandlerError { + requestHandlerError { + code + cmd + signal + statusCode + statusMessage } }` }[event]; diff --git a/src/main.ts b/src/main.ts index c90e95f0e..781f5c16a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -142,4 +142,6 @@ export * as PluggableAdmin from './pluggable-admin-api/pluggable-admin'; * other plugins. * @category Internal */ -export * as MockttpPluggableAdmin from './pluggable-admin-api/mockttp-pluggable-admin'; \ No newline at end of file +export * as MockttpPluggableAdmin from './pluggable-admin-api/mockttp-pluggable-admin'; + +export { RequestHandlerError } from "./types"; diff --git a/src/mockttp.ts b/src/mockttp.ts index ad9cb9304..43afb9e00 100644 --- a/src/mockttp.ts +++ b/src/mockttp.ts @@ -15,11 +15,11 @@ import { TlsRequest, InitiatedRequest, ClientError, - RulePriority + RulePriority, + RequestHandlerError } from "./types"; import type { RequestRuleData } from "./rules/requests/request-rule"; import type { WebSocketRuleData } from "./rules/websockets/websocket-rule"; -import { ErrorLike } from "./util/error"; export type PortRange = { startPort: number, endPort: number }; @@ -417,13 +417,9 @@ export interface Mockttp { on(event: 'client-error', callback: (error: ClientError) => void): Promise; /** - * Subscribe to hear about requests that fail before successfully sending their - * initial parameters (the request line & headers). This will fire for requests - * that drop connections early, send invalid or too-long headers, or aren't - * correctly parseable in some form. + * Subscribe to hear about requests that fail during request handling. * - * This is only useful in some niche use cases, such as logging all requests - * seen by the server, independently of the rules defined. + * This is useful in case of DNS resolution problems or connecting to the remote peer. * * The callback will be called asynchronously from request handling. This function * returns a promise, and the callback is not guaranteed to be registered until @@ -431,7 +427,7 @@ export interface Mockttp { * * @category Events */ - on(event: 'handle-error', callback: (error: ErrorLike) => void): Promise; + on(event: 'request-handler-error', callback: (error: RequestHandlerError) => void): Promise; /** * Adds the given rules to the server. @@ -595,7 +591,7 @@ export type SubscribableEvent = | 'abort' | 'tls-client-error' | 'client-error' - | 'handle-error'; + | 'request-handler-error'; /** * @hidden diff --git a/src/rules/requests/request-handler-definitions.ts b/src/rules/requests/request-handler-definitions.ts index 5b49baa35..8811415d6 100644 --- a/src/rules/requests/request-handler-definitions.ts +++ b/src/rules/requests/request-handler-definitions.ts @@ -985,4 +985,4 @@ export const HandlerDefinitionLookup = { 'passthrough': PassThroughHandlerDefinition, 'close-connection': CloseConnectionHandlerDefinition, 'timeout': TimeoutHandlerDefinition -} \ No newline at end of file +} diff --git a/src/rules/requests/request-handlers.ts b/src/rules/requests/request-handlers.ts index c2c22c65b..7ebd46d6a 100644 --- a/src/rules/requests/request-handlers.ts +++ b/src/rules/requests/request-handlers.ts @@ -1025,4 +1025,4 @@ export const HandlerLookup: typeof HandlerDefinitionLookup = { 'passthrough': PassThroughHandler, 'close-connection': CloseConnectionHandler, 'timeout': TimeoutHandler -} \ No newline at end of file +} diff --git a/src/server/mockttp-server.ts b/src/server/mockttp-server.ts index f89e43d62..f8aa4f995 100644 --- a/src/server/mockttp-server.ts +++ b/src/server/mockttp-server.ts @@ -20,7 +20,8 @@ import { TlsRequest, ClientError, TimingEvents, - OngoingBody + OngoingBody, + RequestHandlerError } from "../types"; import { CAOptions } from '../util/tls'; import { DestroyableServer } from "../util/destroyable-server"; @@ -30,7 +31,7 @@ import { ServerMockedEndpoint } from "./mocked-endpoint"; import { createComboServer } from "./http-combo-server"; import { filter } from "../util/promise"; import { Mutable } from "../util/type-utils"; -import { ErrorLike, isErrorLike } from "../util/error"; +import { isErrorLike } from "../util/error"; import { makePropertyWritable } from "../util/util"; import { @@ -277,7 +278,7 @@ export class MockttpServer extends AbstractMockttp implements Mockttp { public on(event: 'tls-client-error', callback: (req: TlsRequest) => void): Promise; public on(event: 'tlsClientError', callback: (req: TlsRequest) => void): Promise; public on(event: 'client-error', callback: (error: ClientError) => void): Promise; - public on(event: 'handle-error', callback: (error: ErrorLike) => void): Promise; + public on(event: 'request-handler-error', callback: (error: RequestHandlerError) => void): Promise; public on(event: string, callback: (...args: any[]) => void): Promise { this.eventEmitter.on(event, callback); return Promise.resolve(); @@ -465,8 +466,8 @@ export class MockttpServer extends AbstractMockttp implements Mockttp { } result = result || 'responded'; } catch (e) { - if (this.eventEmitter.listeners('handle-error').length > 0) { - this.eventEmitter.emit('handle-error', e); + if (this.eventEmitter.listeners('requerst-handle-error').length > 0) { + this.eventEmitter.emit('requerst-handle-error', e); } else { if (e instanceof AbortError) { diff --git a/src/types.ts b/src/types.ts index f111874cd..64aa34100 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,7 @@ import stream = require('stream'); import http = require('http'); import { EventEmitter } from 'events'; +import { ErrorLike } from './util/error'; export const DEFAULT_ADMIN_SERVER_PORT = 45454; @@ -205,6 +206,11 @@ export interface ClientError { response: CompletedResponse | 'aborted'; } +/** + * RequestHandlerError dummy + */ + export interface RequestHandlerError extends ErrorLike {} + /** * A mocked endpoint provides methods to see the current state of * a mock rule. diff --git a/src/util/error.ts b/src/util/error.ts index aaa1b5ce7..19a02275f 100644 --- a/src/util/error.ts +++ b/src/util/error.ts @@ -1,4 +1,3 @@ -// A server error export type ErrorLike = Partial & { // Various properties we might want to look for on errors: code?: string;