Skip to content

Commit b1cd2dd

Browse files
TimothyGubseber
andcommitted
Better compliance with Web IDL
- Make read-only attributes actually read-only - Set @@toStringTag on the prototype only - Make prototype methods/getters enumerable Based on node-fetch#354. Co-authored-by: Benjamin Seber <seber@synyx.de>
1 parent dccef32 commit b1cd2dd

File tree

7 files changed

+230
-92
lines changed

7 files changed

+230
-92
lines changed

src/blob.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,6 @@ const TYPE = Symbol('type');
66

77
export default class Blob {
88
constructor() {
9-
Object.defineProperty(this, Symbol.toStringTag, {
10-
value: 'Blob',
11-
writable: false,
12-
enumerable: false,
13-
configurable: true
14-
});
15-
169
this[TYPE] = '';
1710

1811
const blobParts = arguments[0];
@@ -87,8 +80,14 @@ export default class Blob {
8780
}
8881
}
8982

83+
Object.defineProperties(Blob.prototype, {
84+
size: { enumerable: true },
85+
type: { enumerable: true },
86+
slice: { enumerable: true }
87+
});
88+
9089
Object.defineProperty(Blob.prototype, Symbol.toStringTag, {
91-
value: 'BlobPrototype',
90+
value: 'Blob',
9291
writable: false,
9392
enumerable: false,
9493
configurable: true

src/body.js

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@ import FetchError from './fetch-error.js';
1111
const Stream = require('stream');
1212
const { PassThrough } = require('stream');
1313

14-
const DISTURBED = Symbol('disturbed');
15-
const ERROR = Symbol('error');
16-
1714
let convert;
1815
try { convert = require('encoding').convert; } catch(e) {}
1916

17+
const INTERNALS = Symbol('Body internals');
18+
2019
/**
21-
* Body class
20+
* Body mixin
2221
*
23-
* Cannot use ES6 class because Body must be called with .call().
22+
* Ref: https://fetch.spec.whatwg.org/#body
2423
*
2524
* @param Stream body Readable stream
2625
* @param Object opts Response options
@@ -48,22 +47,28 @@ export default function Body(body, {
4847
// coerce to string
4948
body = String(body);
5049
}
51-
this.body = body;
52-
this[DISTURBED] = false;
53-
this[ERROR] = null;
50+
this[INTERNALS] = {
51+
body,
52+
disturbed: false,
53+
error: null
54+
};
5455
this.size = size;
5556
this.timeout = timeout;
5657

57-
if (this.body instanceof Stream) {
58-
this.body.on('error', err => {
59-
this[ERROR] = new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err);
58+
if (body instanceof Stream) {
59+
body.on('error', err => {
60+
this[INTERNALS].error = new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err);
6061
});
6162
}
6263
}
6364

6465
Body.prototype = {
66+
get body() {
67+
return this[INTERNALS].body;
68+
},
69+
6570
get bodyUsed() {
66-
return this[DISTURBED];
71+
return this[INTERNALS].disturbed;
6772
},
6873

6974
/**
@@ -139,6 +144,16 @@ Body.prototype = {
139144

140145
};
141146

147+
// In browsers, all properties are enumerable.
148+
Object.defineProperties(Body.prototype, {
149+
body: { enumerable: true },
150+
bodyUsed: { enumerable: true },
151+
arrayBuffer: { enumerable: true },
152+
blob: { enumerable: true },
153+
json: { enumerable: true },
154+
text: { enumerable: true }
155+
});
156+
142157
Body.mixIn = function (proto) {
143158
for (const name of Object.getOwnPropertyNames(Body.prototype)) {
144159
// istanbul ignore else: future proof
@@ -150,19 +165,21 @@ Body.mixIn = function (proto) {
150165
};
151166

152167
/**
153-
* Decode buffers into utf-8 string
168+
* Consume and convert an entire Body to a Buffer.
169+
*
170+
* Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body
154171
*
155172
* @return Promise
156173
*/
157-
function consumeBody(body) {
158-
if (this[DISTURBED]) {
159-
return Body.Promise.reject(new Error(`body used already for: ${this.url}`));
174+
function consumeBody() {
175+
if (this[INTERNALS].disturbed) {
176+
return Body.Promise.reject(new TypeError(`body used already for: ${this.url}`));
160177
}
161178

162-
this[DISTURBED] = true;
179+
this[INTERNALS].disturbed = true;
163180

164-
if (this[ERROR]) {
165-
return Body.Promise.reject(this[ERROR]);
181+
if (this[INTERNALS].error) {
182+
return Body.Promise.reject(this[INTERNALS].error);
166183
}
167184

168185
// body is null
@@ -309,21 +326,21 @@ function convertBody(buffer, headers) {
309326
* @return String
310327
*/
311328
function isURLSearchParams(obj) {
312-
// Duck-typing as a necessary condition.
313-
if (typeof obj !== 'object' ||
314-
typeof obj.append !== 'function' ||
315-
typeof obj.delete !== 'function' ||
316-
typeof obj.get !== 'function' ||
317-
typeof obj.getAll !== 'function' ||
318-
typeof obj.has !== 'function' ||
319-
typeof obj.set !== 'function') {
320-
return false;
321-
}
322-
323-
// Brand-checking and more duck-typing as optional condition.
324-
return obj.constructor.name === 'URLSearchParams' ||
325-
Object.prototype.toString.call(obj) === '[object URLSearchParams]' ||
326-
typeof obj.sort === 'function';
329+
// Duck-typing as a necessary condition.
330+
if (typeof obj !== 'object' ||
331+
typeof obj.append !== 'function' ||
332+
typeof obj.delete !== 'function' ||
333+
typeof obj.get !== 'function' ||
334+
typeof obj.getAll !== 'function' ||
335+
typeof obj.has !== 'function' ||
336+
typeof obj.set !== 'function') {
337+
return false;
338+
}
339+
340+
// Brand-checking and more duck-typing as optional condition.
341+
return obj.constructor.name === 'URLSearchParams' ||
342+
Object.prototype.toString.call(obj) === '[object URLSearchParams]' ||
343+
typeof obj.sort === 'function';
327344
}
328345

329346
/**
@@ -350,7 +367,7 @@ export function clone(instance) {
350367
body.pipe(p1);
351368
body.pipe(p2);
352369
// set instance body to teed body and return the other teed body
353-
instance.body = p1;
370+
instance[INTERNALS].body = p1;
354371
body = p2;
355372
}
356373

@@ -362,7 +379,7 @@ export function clone(instance) {
362379
* specified in the specification:
363380
* https://fetch.spec.whatwg.org/#concept-bodyinit-extract
364381
*
365-
* This function assumes that instance.body is present and non-null.
382+
* This function assumes that instance.body is present.
366383
*
367384
* @param Mixed instance Response or Request instance
368385
*/

src/headers.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,6 @@ export default class Headers {
8484
} else {
8585
throw new TypeError('Provided initializer must be an object');
8686
}
87-
88-
Object.defineProperty(this, Symbol.toStringTag, {
89-
value: 'Headers',
90-
writable: false,
91-
enumerable: false,
92-
configurable: true
93-
});
9487
}
9588

9689
/**
@@ -214,12 +207,24 @@ export default class Headers {
214207
Headers.prototype.entries = Headers.prototype[Symbol.iterator];
215208

216209
Object.defineProperty(Headers.prototype, Symbol.toStringTag, {
217-
value: 'HeadersPrototype',
210+
value: 'Headers',
218211
writable: false,
219212
enumerable: false,
220213
configurable: true
221214
});
222215

216+
Object.defineProperties(Headers.prototype, {
217+
get: { enumerable: true },
218+
forEach: { enumerable: true },
219+
set: { enumerable: true },
220+
append: { enumerable: true },
221+
has: { enumerable: true },
222+
delete: { enumerable: true },
223+
keys: { enumerable: true },
224+
values: { enumerable: true },
225+
entries: { enumerable: true }
226+
});
227+
223228
function getHeaderPairs(headers, kind) {
224229
const keys = Object.keys(headers[MAP]).sort();
225230
return keys.map(

src/index.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,25 @@ export default function fetch(url, opts) {
8484
return;
8585
}
8686

87+
// Create a new Request object.
88+
const requestOpts = {
89+
headers: new Headers(request.headers),
90+
follow: request.follow,
91+
counter: request.counter + 1,
92+
agent: request.agent,
93+
compress: request.compress,
94+
method: request.method
95+
};
96+
8797
// per fetch spec, for POST request with 301/302 response, or any request with 303 response, use GET when following redirect
8898
if (res.statusCode === 303
8999
|| ((res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST'))
90100
{
91-
request.method = 'GET';
92-
request.body = null;
93-
request.headers.delete('content-length');
101+
requestOpts.method = 'GET';
102+
requestOpts.headers.delete('content-length');
94103
}
95104

96-
request.counter++;
97-
98-
resolve(fetch(resolve_url(request.url, res.headers.location), request));
105+
resolve(fetch(new Request(resolve_url(request.url, res.headers.location), requestOpts)));
99106
return;
100107
}
101108

0 commit comments

Comments
 (0)