Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fetch support delay #2602

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion lib/create_response.js
Expand Up @@ -19,8 +19,9 @@ const responseStatusCodesWithoutBody = [204, 205, 304]

/**
* @param {import('node:http').IncomingMessage} message
* @param {AbortSignal} signal
*/
function createResponse(message) {
function createResponse(message, signal) {
// https://github.com/Uzlopak/undici/blob/main/lib/fetch/index.js#L2031
const decoders = []
const codings =
Expand Down Expand Up @@ -54,6 +55,10 @@ function createResponse(message) {
? null
: new ReadableStream({
start(controller) {
signal.addEventListener('abort', () => {
message.removeAllListeners()
controller.error(signal.reason)
})
message.on('data', chunk => chunks.push(chunk))
message.on('end', () => {
pipeline(
Expand Down
22 changes: 21 additions & 1 deletion lib/intercept.js
Expand Up @@ -240,6 +240,7 @@ function removeInterceptor(options) {
let originalClientRequest
// Variable where we keep the fetch we have overridden
let originalFetch
let originalAbortTimeout

function ErroringClientRequest(error) {
http.OutgoingMessage.call(this)
Expand Down Expand Up @@ -343,6 +344,8 @@ function restoreOverriddenClientRequest() {

global.fetch = originalFetch
originalFetch = undefined
AbortSignal.timeout = originalAbortTimeout
originalAbortTimeout = undefined

debug('- ClientRequest restored')
}
Expand Down Expand Up @@ -443,18 +446,35 @@ function activate() {
})

originalFetch = global.fetch
originalAbortTimeout = AbortSignal.timeout
AbortSignal.timeout = function nockAbortTimeout(delay) {
const signal = originalAbortTimeout(delay)
signal.__nock_delay = delay
return signal
}
global.fetch = async (input, init = undefined) => {
const request = new Request(input, init)
if (request.signal.aborted) return Promise.reject(request.signal.reason)

const options = common.convertFetchRequestToClientRequest(request)
options.isFetchRequest = true
options.timeout = init?.signal?.__nock_delay
const body = await request.arrayBuffer()
const clientRequest = new http.ClientRequest(options)

// If mock found
if (clientRequest.interceptors) {
return new Promise((resolve, reject) => {
clientRequest.on('timeout', () => {
reject(
new DOMException(
'The operation was aborted due to timeout',
'TimeoutError',
),
)
})
clientRequest.on('response', response => {
resolve(createResponse(response))
resolve(createResponse(response, request.signal))
})
clientRequest.on('error', reject)
clientRequest.end(body)
Expand Down
74 changes: 74 additions & 0 deletions tests/test_fetch.js
Expand Up @@ -111,6 +111,19 @@ describe('Native Fetch', () => {
scope.done()
})

it('should throw when signal is already aborted', async () => {
nock('http://example.test').get('/').reply(200)

const signal = AbortSignal.abort('reason')
try {
await fetch('http://example.test', { signal })
expect.fail()
} catch (e) {
expect(signal.aborted).to.true()
expect(e).to.equal('reason')
}
})

describe('content-encoding', () => {
it('should accept gzipped content', async () => {
const message = 'Lorem ipsum dolor sit amet'
Expand Down Expand Up @@ -216,4 +229,65 @@ describe('Native Fetch', () => {
})
})
})

describe('delay', () => {
it('should cause a timeout error when larger than AbortSignal.timeout', async () => {
nock('http://example.test').get('/').delay(100).reply(200)

try {
await fetch('http://example.test', { signal: AbortSignal.timeout(50) })
expect.fail()
} catch (e) {
expect(e.name).to.equal('TimeoutError')
}
})

it('should delay the connection', async () => {
nock('http://example.test').get('/').delayBody(50).reply(200)

const start = Date.now()

const response = await fetch('http://example.test')

expect(response.status).to.equal(200)
// wait for body stream to complete
await response.text()
expect(Date.now() - start).to.be.at.least(
50,
'delay minimum not satisfied',
)
})

it('should delay the response body', async () => {
nock('http://example.test').get('/').delayBody(50).reply(200)

const start = Date.now()

const response = await fetch('http://example.test', {
signal: AbortSignal.timeout(100),
})

expect(response.status).to.equal(200)
// wait for body stream to complete
await response.text()
expect(Date.now() - start).to.be.at.least(
50,
'delay minimum not satisfied',
)
})

it('should throw TimeoutError', async () => {
nock('http://example.test').get('/').delayBody(100).reply(200)

const response = await fetch('http://example.test', {
signal: AbortSignal.timeout(50),
})
try {
await response.text()
expect.fail()
} catch (e) {
expect(e.name).to.equal('TimeoutError')
}
})
})
})