Skip to content

Commit ad977a9

Browse files
authored
fix: fix headers and abort signals (#41)
- make sure we honour headers passed as api and constructor args - do not share abort signals between requests
1 parent ad2fdc4 commit ad977a9

File tree

3 files changed

+65
-30
lines changed

3 files changed

+65
-30
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"license": "MIT",
3434
"dependencies": {
3535
"abort-controller": "^3.0.0",
36+
"any-signal": "^1.1.0",
3637
"buffer": "^5.4.2",
3738
"err-code": "^2.0.0",
3839
"fs-extra": "^9.0.0",
@@ -45,7 +46,7 @@
4546
"stream-to-it": "^0.2.0"
4647
},
4748
"devDependencies": {
48-
"aegir": "^21.8.0",
49+
"aegir": "^21.10.0",
4950
"delay": "^4.3.0",
5051
"it-all": "^1.0.1",
5152
"it-drain": "^1.0.0",

src/http.js

+42-29
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
'use strict'
33

44
const fetch = require('node-fetch')
5-
const merge = require('merge-options')
5+
const merge = require('merge-options').bind({ ignoreUndefined: true })
66
const { URL, URLSearchParams } = require('iso-url')
77
const TextDecoder = require('./text-encoder')
88
const AbortController = require('abort-controller')
9+
const anySignal = require('any-signal')
910

1011
const Request = fetch.Request
1112
const Headers = fetch.Headers
@@ -32,24 +33,32 @@ const timeout = (promise, ms, abortController) => {
3233

3334
const start = Date.now()
3435

36+
const timedOut = () => {
37+
const time = Date.now() - start
38+
39+
return time >= ms
40+
}
41+
3542
return new Promise((resolve, reject) => {
43+
const timeoutID = setTimeout(() => {
44+
if (timedOut()) {
45+
reject(new TimeoutError())
46+
abortController.abort()
47+
}
48+
}, ms)
49+
3650
const after = (next) => {
3751
return (res) => {
3852
clearTimeout(timeoutID)
39-
const time = Date.now() - start
4053

41-
if (time >= ms) {
42-
abortController.abort()
54+
if (timedOut()) {
4355
reject(new TimeoutError())
4456
return
4557
}
4658

47-
if (next) {
48-
next(res)
49-
}
59+
next(res)
5060
}
5161
}
52-
const timeoutID = setTimeout(after(), ms)
5362

5463
promise
5564
.then(after(resolve), after(reject))
@@ -88,18 +97,6 @@ class HTTP {
8897
constructor (options = {}) {
8998
/** @type {APIOptions} */
9099
this.opts = merge(defaults, options)
91-
this.opts.headers = new Headers(options.headers)
92-
93-
// connect internal abort to external
94-
this.abortController = new AbortController()
95-
96-
if (this.opts.signal) {
97-
this.opts.signal.addEventListener('abort', () => {
98-
this.abortController.abort()
99-
})
100-
}
101-
102-
this.opts.signal = this.abortController.signal
103100
}
104101

105102
/**
@@ -144,13 +141,14 @@ class HTTP {
144141
opts.headers.set('content-type', 'application/json')
145142
}
146143

144+
const abortController = new AbortController()
145+
const signal = anySignal([abortController.signal, opts.signal])
146+
147147
const response = await timeout(fetch(url, {
148148
...opts,
149-
150-
// node-fetch implements it's own timeout in an addition to the spec so do not
151-
// pass the timeout value on, otherwise there are two sources of timeout errors
149+
signal,
152150
timeout: undefined
153-
}), opts.timeout, this.abortController)
151+
}), opts.timeout, abortController)
154152

155153
if (!response.ok && opts.throwHttpErrors) {
156154
if (opts.handleError) {
@@ -188,7 +186,10 @@ class HTTP {
188186
* @returns {Promise<Response>}
189187
*/
190188
post (resource, options = {}) {
191-
return this.fetch(resource, merge(this.opts, options, { method: 'POST' }))
189+
return this.fetch(resource, {
190+
...options,
191+
method: 'POST'
192+
})
192193
}
193194

194195
/**
@@ -197,7 +198,10 @@ class HTTP {
197198
* @returns {Promise<Response>}
198199
*/
199200
get (resource, options = {}) {
200-
return this.fetch(resource, merge(this.opts, options, { method: 'GET' }))
201+
return this.fetch(resource, {
202+
...options,
203+
method: 'GET'
204+
})
201205
}
202206

203207
/**
@@ -206,7 +210,10 @@ class HTTP {
206210
* @returns {Promise<Response>}
207211
*/
208212
put (resource, options = {}) {
209-
return this.fetch(resource, merge(this.opts, options, { method: 'PUT' }))
213+
return this.fetch(resource, {
214+
...options,
215+
method: 'PUT'
216+
})
210217
}
211218

212219
/**
@@ -215,7 +222,10 @@ class HTTP {
215222
* @returns {Promise<Response>}
216223
*/
217224
delete (resource, options = {}) {
218-
return this.fetch(resource, merge(this.opts, options, { method: 'DELETE' }))
225+
return this.fetch(resource, {
226+
...options,
227+
method: 'DELETE'
228+
})
219229
}
220230

221231
/**
@@ -224,7 +234,10 @@ class HTTP {
224234
* @returns {Promise<Response>}
225235
*/
226236
options (resource, options = {}) {
227-
return this.fetch(resource, merge(this.opts, options, { method: 'OPTIONS' }))
237+
return this.fetch(resource, {
238+
...options,
239+
method: 'OPTIONS'
240+
})
228241
}
229242
}
230243

test/http.spec.js

+21
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,27 @@ describe('http', function () {
3030
})).to.eventually.be.rejectedWith().instanceOf(HTTP.TimeoutError)
3131
})
3232

33+
it('respects headers', async function () {
34+
const req = await HTTP.post(`${process.env.ECHO_SERVER}/echo/headers`, {
35+
headers: {
36+
foo: 'bar'
37+
}
38+
})
39+
const rsp = await req.json()
40+
expect(rsp).to.have.property('foo', 'bar')
41+
})
42+
43+
it('respects constructor headers', async function () {
44+
const http = new HTTP({
45+
headers: {
46+
bar: 'baz'
47+
}
48+
})
49+
const req = await http.post(`${process.env.ECHO_SERVER}/echo/headers`)
50+
const rsp = await req.json()
51+
expect(rsp).to.have.property('bar', 'baz')
52+
})
53+
3354
it('makes a JSON request', async () => {
3455
const req = await HTTP.post(`${process.env.ECHO_SERVER}/echo`, {
3556
json: {

0 commit comments

Comments
 (0)