diff --git a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts index 90a2b59212..7f187d1b80 100644 --- a/packages/driver/cypress/integration/commands/net_stubbing_spec.ts +++ b/packages/driver/cypress/integration/commands/net_stubbing_spec.ts @@ -1839,13 +1839,7 @@ describe('network stubbing', { retries: 2 }, function () { it('doesn\'t fail test if network error occurs retrieving response and response is not intercepted', { // TODO: for some reason, this test is busted in FF browser: '!firefox', - }, function (done) { - cy.on('fail', (err) => { - // the test should have failed due to cy.wait, as opposed to because of a network error - expect(err.message).to.contain('Timed out retrying') - done() - }) - + }, function () { cy.intercept('/should-err', function (req) { req.reply() }) @@ -2149,6 +2143,21 @@ describe('network stubbing', { retries: 2 }, function () { }) }) + // @see https://github.com/cypress-io/cypress/issues/9062 + it('can spy on a request using forceNetworkError', function () { + cy.intercept('/foo', { forceNetworkError: true }) + .as('err') + .then(() => { + $.get('/foo') + }) + .wait('@err').should('have.property', 'error') + .and('include', { + message: 'forceNetworkError called', + name: 'Error', + }) + .get('@err').should('not.have.property', 'response') + }) + context('with an intercepted request', function () { it('can dynamically alias the request', function () { cy.intercept('/foo', (req) => { diff --git a/packages/driver/src/cy/net-stubbing/events/request-complete.ts b/packages/driver/src/cy/net-stubbing/events/request-complete.ts index d34c9050ec..26c5b5d3f4 100644 --- a/packages/driver/src/cy/net-stubbing/events/request-complete.ts +++ b/packages/driver/src/cy/net-stubbing/events/request-complete.ts @@ -11,20 +11,28 @@ export const onRequestComplete: HandlerFn = } if (frame.error) { + let err = makeErrFromObj(frame.error) + // does this request have a responseHandler that has not run yet? + const isAwaitingResponse = !!request.responseHandler && ['Received', 'Intercepted'].includes(request.state) const isTimeoutError = frame.error.code && ['ESOCKETTIMEDOUT', 'ETIMEDOUT'].includes(frame.error.code) - const errorName = isTimeoutError ? 'timeout' : 'network_error' - const err = errByPath(`net_stubbing.request_error.${errorName}`, { - innerErr: makeErrFromObj(frame.error), - req: request.request, - route: get(getRoute(frame.routeHandlerId), 'options'), - }) + if (isAwaitingResponse || isTimeoutError) { + const errorName = isTimeoutError ? 'timeout' : 'network_error' + + err = errByPath(`net_stubbing.request_error.${errorName}`, { + innerErr: err, + req: request.request, + route: get(getRoute(frame.routeHandlerId), 'options'), + }) + } request.state = 'Errored' + request.error = err - if (request.responseHandler) { - // if req.reply was used to register a response handler, the user is implicitly - // expecting there to be a successful response from the server, so fail the test + request.log.error(err) + + if (isAwaitingResponse) { + // the user is implicitly expecting there to be a successful response from the server, so fail the test // since a network error has occured return failCurrentTest(err) } diff --git a/packages/driver/src/cy/net-stubbing/wait-for-route.ts b/packages/driver/src/cy/net-stubbing/wait-for-route.ts index c9f7fc8552..b9823f4698 100644 --- a/packages/driver/src/cy/net-stubbing/wait-for-route.ts +++ b/packages/driver/src/cy/net-stubbing/wait-for-route.ts @@ -5,7 +5,7 @@ import { } from './types' import { getAliasedRequests } from './aliasing' -const RESPONSE_WAITED_STATES: InterceptionState[] = ['ResponseIntercepted', 'Complete'] +const RESPONSE_WAITED_STATES: InterceptionState[] = ['ResponseIntercepted', 'Complete', 'Errored'] function getPredicateForSpecifier (specifier: string): Partial { if (specifier === 'request') { diff --git a/packages/net-stubbing/lib/external-types.ts b/packages/net-stubbing/lib/external-types.ts index 34a010048a..f26e2f120b 100644 --- a/packages/net-stubbing/lib/external-types.ts +++ b/packages/net-stubbing/lib/external-types.ts @@ -192,6 +192,10 @@ export interface Interception { response?: CyHttpMessages.IncomingResponse /* @internal */ responseHandler?: HttpResponseInterceptor + /** + * The error that occurred during this request. + */ + error?: Error /** * Was `cy.wait()` used to wait on the response to this request? * @internal diff --git a/packages/net-stubbing/lib/server/intercept-request.ts b/packages/net-stubbing/lib/server/intercept-request.ts index 4064d844c6..88a8f05db0 100644 --- a/packages/net-stubbing/lib/server/intercept-request.ts +++ b/packages/net-stubbing/lib/server/intercept-request.ts @@ -51,6 +51,7 @@ export const InterceptRequest: RequestMiddleware = function () { requestId, route, continueRequest: this.next, + onError: this.onError, onResponse: (incomingRes, resStream) => { setDefaultHeaders(this.req, incomingRes) this.onResponse(incomingRes, resStream) @@ -109,7 +110,7 @@ function _interceptRequest (state: NetStubbingState, request: BackendRequest, ro return ensureBody(() => { emitReceived() - sendStaticResponse(request.res, staticResponse, request.onResponse!) + sendStaticResponse(request, staticResponse) }) } @@ -183,7 +184,7 @@ export async function onRequestContinue (state: NetStubbingState, frame: NetEven if (frame.staticResponse) { await setResponseFromFixture(backendRequest.route.getFixture, frame.staticResponse) - return sendStaticResponse(backendRequest.res, frame.staticResponse, backendRequest.onResponse!) + return sendStaticResponse(backendRequest, frame.staticResponse) } backendRequest.continueRequest() diff --git a/packages/net-stubbing/lib/server/intercept-response.ts b/packages/net-stubbing/lib/server/intercept-response.ts index 0af377122a..cc5da6d7e5 100644 --- a/packages/net-stubbing/lib/server/intercept-response.ts +++ b/packages/net-stubbing/lib/server/intercept-response.ts @@ -111,7 +111,7 @@ export async function onResponseContinue (state: NetStubbingState, frame: NetEve const staticResponse = _.chain(frame.staticResponse).clone().assign({ continueResponseAt, throttleKbps }).value() - return sendStaticResponse(res, staticResponse, backendRequest.onResponse!) + return sendStaticResponse(backendRequest, staticResponse) } // merge the changed response attributes with our response and continue diff --git a/packages/net-stubbing/lib/server/types.ts b/packages/net-stubbing/lib/server/types.ts index a525d5fc36..c5c82fd4d2 100644 --- a/packages/net-stubbing/lib/server/types.ts +++ b/packages/net-stubbing/lib/server/types.ts @@ -25,6 +25,7 @@ export interface BackendRequest { * The route that matched this request. */ route: BackendRoute + onError: (err: Error) => void /** * A callback that can be used to make the request go outbound. */ diff --git a/packages/net-stubbing/lib/server/util.ts b/packages/net-stubbing/lib/server/util.ts index 5b67e1a001..24576bf167 100644 --- a/packages/net-stubbing/lib/server/util.ts +++ b/packages/net-stubbing/lib/server/util.ts @@ -1,7 +1,7 @@ import _ from 'lodash' import Debug from 'debug' import isHtml from 'is-html' -import { ServerResponse, IncomingMessage } from 'http' +import { IncomingMessage } from 'http' import { RouteMatcherOptionsGeneric, STRING_MATCHER_FIELDS, @@ -11,7 +11,7 @@ import { import { Readable, PassThrough } from 'stream' import CyServer from '@packages/server' import { Socket } from 'net' -import { GetFixtureFn } from './types' +import { GetFixtureFn, BackendRequest } from './types' import ThrottleStream from 'throttle' import MimeTypes from 'mime-types' @@ -142,17 +142,17 @@ export async function setResponseFromFixture (getFixtureFn: GetFixtureFn, static /** * Using an existing response object, send a response shaped by a StaticResponse object. - * @param res Response object. + * @param backendRequest BackendRequest object. * @param staticResponse BackendStaticResponse object. - * @param onResponse Will be called with the response metadata + body stream - * @param resStream Optionally, provide a Readable stream to be used as the response body (overrides staticResponse.body) */ -export function sendStaticResponse (res: ServerResponse, staticResponse: BackendStaticResponse, onResponse: (incomingRes: IncomingMessage, stream: Readable) => void) { - if (staticResponse.forceNetworkError) { - res.connection.destroy() - res.destroy() +export function sendStaticResponse (backendRequest: BackendRequest, staticResponse: BackendStaticResponse) { + const { onError, onResponse } = backendRequest - return + if (staticResponse.forceNetworkError) { + debug('forcing network error') + const err = new Error('forceNetworkError called') + + return onError(err) } const statusCode = staticResponse.statusCode || 200 @@ -167,7 +167,7 @@ export function sendStaticResponse (res: ServerResponse, staticResponse: Backend const bodyStream = getBodyStream(body, _.pick(staticResponse, 'throttleKbps', 'continueResponseAt')) - onResponse(incomingRes, bodyStream) + onResponse!(incomingRes, bodyStream) } export function getBodyStream (body: Buffer | string | Readable | undefined, options: { continueResponseAt?: number, throttleKbps?: number }): Readable {