diff --git a/src/interceptors/ClientRequest/NodeClientRequest.ts b/src/interceptors/ClientRequest/NodeClientRequest.ts index 0bdec77f..b479a845 100644 --- a/src/interceptors/ClientRequest/NodeClientRequest.ts +++ b/src/interceptors/ClientRequest/NodeClientRequest.ts @@ -20,6 +20,7 @@ import { toInteractiveRequest } from '../../utils/toInteractiveRequest' import { uuidv4 } from '../../utils/uuid' import { emitAsync } from '../../utils/emitAsync' import { getRawFetchHeaders } from '../../utils/getRawFetchHeaders' +import { NodeError } from '../../utils/nodeError' import { isPropertyAccessible } from '../../utils/isPropertyAccessible' export type Protocol = 'http' | 'https' @@ -120,6 +121,13 @@ export class NodeClientRequest extends ClientRequest { } write(...args: ClientRequestWriteArgs): boolean { + if (this.state >= HttpClientInternalState.Sent) { + const err = new NodeError('write after end', 'ERR_STREAM_WRITE_AFTER_END') + process.nextTick(() => this.emit('error', err)) + + return false + } + const [chunk, encoding, callback] = normalizeClientRequestWriteArgs(args) this.logger.info('write:', { chunk, encoding, callback }) this.chunks.push({ chunk, encoding }) diff --git a/src/utils/nodeError.ts b/src/utils/nodeError.ts new file mode 100644 index 00000000..5b5f0a36 --- /dev/null +++ b/src/utils/nodeError.ts @@ -0,0 +1,5 @@ +export class NodeError extends Error { + constructor(readonly message: string, readonly code: string) { + super(message) + } +} \ No newline at end of file diff --git a/test/modules/http/compliance/http-req-end-then-write.test.ts b/test/modules/http/compliance/http-req-end-then-write.test.ts new file mode 100644 index 00000000..ac48efac --- /dev/null +++ b/test/modules/http/compliance/http-req-end-then-write.test.ts @@ -0,0 +1,40 @@ +import { it, expect, beforeAll, afterAll } from 'vitest' +import http from 'http' +import express from 'express' +import { HttpServer } from '@open-draft/test-server/http' +import { DeferredPromise } from '@open-draft/deferred-promise' +import { ClientRequestInterceptor } from '../../../../src/interceptors/ClientRequest' + +const httpServer = new HttpServer((app) => { + app.post('/resource', express.text({ type: '*/*' }), (req, res) => { + res.send(req.body) + }) +}) + +const interceptor = new ClientRequestInterceptor() + +beforeAll(async () => { + interceptor.apply() + await httpServer.listen() +}) + +afterAll(async () => { + interceptor.dispose() + await httpServer.close() +}) + +it('emits the ERR_STREAM_WRITE_AFTER_END error when write after end given no mocked response', async () => { + const req = http.request(httpServer.http.url('/resource')) + + const errorReceived = new DeferredPromise() + req.on('error', (error) => { + errorReceived.resolve(error) + }) + + req.end() + req.write('foo') + + const error = await errorReceived + + expect(error.code).toBe('ERR_STREAM_WRITE_AFTER_END') +}) \ No newline at end of file