Repository: node-fetch/node-fetch
Pull request #785 (2020-04-23 12:45:57)

Move to Github Actions


Patch:
diff --git a/.editorconfig b/.editorconfig
index 991f40f..d324ccc 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -11,3 +11,6 @@ indent_style = tab
 
 [*.md]
 trim_trailing_whitespace = false
+
+[*.yml]
+indent_style = space
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..3975315
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,40 @@
+name: Node.js CI
+
+on:
+  [pull_request]
+
+jobs:
+  build:
+
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-latest, macOS-latest]
+        node: [14.x, 12.x, 10.x]
+        exclude:
+          # On Windows, run tests with only the LTS environments.
+          - os: windows-latest
+            node: 10.x
+          - os: windows-latest
+            node: 14.x
+          # On macOS, run tests with only the LTS environments.
+          - os: macOS-latest
+            node: 10.x
+          - os: macOS-latest
+            node: 14.x
+
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-node@v1
+        with:
+          node-version: ${{ matrix.node-version }}
+      - run: npm test
+        if: matrix.node-version != '10.x'
+      - run: npm run coverage
+        if: matrix.node-version == '14.x' && matrix.os == 'ubuntu-latest'
+
+      - name: Test without coverage
+        if: matrix.node-version == '10.x'
+        run: |
+           npm install -D esm
+           npx mocha -r esm
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..61b5002
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,25 @@
+name: Lint
+
+on:
+  pull_request:
+    paths:
+      - '*.js'
+      - '**eslint**'
+      - 'package.json'
+      - '.github/workflows/lint.yml'
+jobs:
+  xo:
+
+    runs-on: [ubuntu-latest]
+
+    strategy:
+      matrix:
+        node-version: [12.x]
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - run: npm run lint
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index f1fc005..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-language: node_js
-
-node_js:
-  - "lts/*" # Latest LTS
-  - "node" # Latest Stable
-
-matrix:
-  include:
-    - # Linting stage
-      node_js: "lts/*" # Latest LTS
-      script: npm run lint
-
-cache: npm
-
-after_success: npm run coverage
diff --git a/LICENSE.md b/LICENSE.md
index 660ffec..41ca1b6 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2016 David Frank
+Copyright (c) 2016 - 2020 Node Fetch Team
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 62677c3..b00e31c 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
   	<img src="docs/media/Banner.svg" alt="Node Fetch"/>
   	<br>
   	<p>A light-weight module that brings <code>window.fetch</code> to Node.js.</p>
-	<a href="https://travis-ci.com/node-fetch/node-fetch"><img src="https://img.shields.io/travis/com/node-fetch/node-fetch/master?style=flat-square" alt="Build status"></a>
+	<a href="https://github.com/node-fetch/node-fetch/actions"><img src="https://github.com/node-fetch/node-fetch/workflows/.github/workflows/ci.yml/badge.svg" alt="Build status"></a>
 	<a href="https://coveralls.io/github/node-fetch/node-fetch"><img src="https://img.shields.io/coveralls/github/node-fetch/node-fetch?style=flat-square" alt="Coverage status"></a>
 	<a href="https://packagephobia.now.sh/result?p=node-fetch"><img src="https://flat.badgen.net/packagephobia/install/node-fetch" alt="Current version"></a>
 	<a href="https://www.npmjs.com/package/node-fetch"><img src="https://img.shields.io/npm/v/node-fetch?style=flat-square" alt="Install size"></a>
@@ -27,50 +27,50 @@
 - [Loading and configuring the module](#loading-and-configuring-the-module)
 - [Upgrading](#upgrading)
 - [Common Usage](#common-usage)
-    - [Plain text or HTML](#plain-text-or-html)
-    - [JSON](#json)
-    - [Simple Post](#simple-post)
-    - [Post with JSON](#post-with-json)
-    - [Post with form parameters](#post-with-form-parameters)
-    - [Handling exceptions](#handling-exceptions)
-    - [Handling client and server errors](#handling-client-and-server-errors)
-    - [Handling cookies](#handling-cookies)
+	- [Plain text or HTML](#plain-text-or-html)
+	- [JSON](#json)
+	- [Simple Post](#simple-post)
+	- [Post with JSON](#post-with-json)
+	- [Post with form parameters](#post-with-form-parameters)
+	- [Handling exceptions](#handling-exceptions)
+	- [Handling client and server errors](#handling-client-and-server-errors)
+	- [Handling cookies](#handling-cookies)
 - [Advanced Usage](#advanced-usage)
-    - [Streams](#streams)
-    - [Buffer](#buffer)
-    - [Accessing Headers and other Meta data](#accessing-headers-and-other-meta-data)
-    - [Extract Set-Cookie Header](#extract-set-cookie-header)
-    - [Post data using a file stream](#post-data-using-a-file-stream)
-    - [Post with form-data (detect multipart)](#post-with-form-data-detect-multipart)
-    - [Request cancellation with AbortSignal](#request-cancellation-with-abortsignal)
+	- [Streams](#streams)
+	- [Buffer](#buffer)
+	- [Accessing Headers and other Meta data](#accessing-headers-and-other-meta-data)
+	- [Extract Set-Cookie Header](#extract-set-cookie-header)
+	- [Post data using a file stream](#post-data-using-a-file-stream)
+	- [Post with form-data (detect multipart)](#post-with-form-data-detect-multipart)
+	- [Request cancellation with AbortSignal](#request-cancellation-with-abortsignal)
 - [API](#api)
-    - [fetch(url[, options])](#fetchurl-options)
-    - [Options](#options)
-        - [Default Headers](#default-headers)
-        - [Custom Agent](#custom-agent)
-        - [Custom highWaterMark](#custom-highwatermark)
-    - [Class: Request](#class-request)
-        - [new Request(input[, options])](#new-requestinput-options)
-    - [Class: Response](#class-response)
-        - [new Response([body[, options]])](#new-responsebody-options)
-        - [response.ok](#responseok)
-        - [response.redirected](#responseredirected)
-    - [Class: Headers](#class-headers)
-        - [new Headers([init])](#new-headersinit)
-    - [Interface: Body](#interface-body)
-        - [body.body](#bodybody)
-        - [body.bodyUsed](#bodybodyused)
-        - [body.arrayBuffer()](#bodyarraybuffer)
-        - [body.blob()](#bodyblob)
-        - [body.json()](#bodyjson)
-        - [body.text()](#bodytext)
-        - [body.buffer()](#bodybuffer)
-    - [Class: FetchError](#class-fetcherror)
-    - [Class: AbortError](#class-aborterror)
+	- [fetch(url[, options])](#fetchurl-options)
+	- [Options](#options)
+		- [Default Headers](#default-headers)
+		- [Custom Agent](#custom-agent)
+		- [Custom highWaterMark](#custom-highwatermark)
+	- [Class: Request](#class-request)
+		- [new Request(input[, options])](#new-requestinput-options)
+	- [Class: Response](#class-response)
+		- [new Response([body[, options]])](#new-responsebody-options)
+		- [response.ok](#responseok)
+		- [response.redirected](#responseredirected)
+	- [Class: Headers](#class-headers)
+		- [new Headers([init])](#new-headersinit)
+	- [Interface: Body](#interface-body)
+		- [body.body](#bodybody)
+		- [body.bodyUsed](#bodybodyused)
+		- [body.arrayBuffer()](#bodyarraybuffer)
+		- [body.blob()](#bodyblob)
+		- [body.json()](#bodyjson)
+		- [body.text()](#bodytext)
+		- [body.buffer()](#bodybuffer)
+	- [Class: FetchError](#class-fetcherror)
+	- [Class: AbortError](#class-aborterror)
 - [TypeScript](#typescript)
 - [Acknowledgement](#acknowledgement)
 - [Team](#team)
-                - [Former](#former)
+				- [Former](#former)
 - [License](#license)
 
 <!-- /TOC -->
@@ -135,21 +135,7 @@ if (!globalThis.fetch) {
 }
 ```
 
-For versions of node earlier than 12.x, use this `globalThis` [polyfill](https://mathiasbynens.be/notes/globalthis):
-
-```js
-(function() {
-	if (typeof globalThis === 'object') return;
-	Object.defineProperty(Object.prototype, '__magic__', {
-		get: function() {
-			return this;
-		},
-		configurable: true
-	});
-	__magic__.globalThis = __magic__;
-	delete Object.prototype.__magic__;
-}());
-```
+For versions of Node earlier than 12, use this `globalThis` [polyfill](https://mathiasbynens.be/notes/globalthis).
 
 ## Upgrading
 
@@ -294,7 +280,7 @@ const fileType = require('file-type');
 
 fetch('https://octodex.github.com/images/Fintechtocat.png')
 	.then(res => res.buffer())
-	.then(buffer => fileType(buffer))
+	.then(buffer => fileType.fromBuffer(buffer))
 	.then(type => {
 		console.log(type);
 	});
@@ -491,7 +477,7 @@ const options = {
 
 #### Custom highWaterMark
 
-Stream on Node.js have a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers). Because of that, when you are writing an isomorphic app and using `res.clone()`, it will hang with large response in Node.
+Stream on Node.js have a smaller internal buffer size (16kB, aka `highWaterMark`) from client-side browsers (>1MB, not consistent across browsers). Because of that, when you are writing an isomorphic app and using `res.clone()`, it will hang with large response in Node.
 
 The recommended way to fix this problem is to resolve cloned response in parallel:
 
@@ -513,7 +499,10 @@ If for some reason you don't like the solution above, since `3.x` you are able t
 ```js
 const fetch = require('node-fetch');
 
-fetch('https://example.com', {highWaterMark: 10}).then(res => res.clone().buffer());
+fetch('https://example.com', {
+	// About 1MB
+	highWaterMark: 1024 * 1024
+}).then(res => res.clone().buffer());
 ```
 
 <a id="class-request"></a>
@@ -693,7 +682,7 @@ An Error thrown when the request is aborted in response to an `AbortSignal`'s `a
 
 ## TypeScript
 
-Since `3.x` types are bundled with `node-fetch`, so you don't need to install any additional packages.
+**Since `3.x` types are bundled with `node-fetch`, so you don't need to install any additional packages.**
 
 For older versions please use the type definitions from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped):
 
@@ -718,7 +707,7 @@ Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid
 
 ## License
 
-MIT
+[MIT](LICENSE.md)
 
 [whatwg-fetch]: https://fetch.spec.whatwg.org/
 [response-init]: https://fetch.spec.whatwg.org/#responseinit
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index b4ed4c5..2655ba6 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -3,6 +3,16 @@ Changelog
 
 # 3.x release
 
+## v3.0.0-beta.6
+
+**Work in progress!**
+
+- Enhance: improve coverage.
+- Enhance: drop Babel (while keeping ESM) (#805).
+- Fix: export the `AbortError` class.
+- Fix: example using `file-type` (#804).
+- Other: readme update.
+
 ## v3.0.0-beta.5
 
 > NOTE: Since the previous beta version included serious issues, such as [#749](https://github.com/node-fetch/node-fetch/issues/749), they will now be deprecated. 
diff --git a/docs/v3-LIMITS.md b/docs/v3-LIMITS.md
index 4204533..a53202e 100644
--- a/docs/v3-LIMITS.md
+++ b/docs/v3-LIMITS.md
@@ -1,4 +1,3 @@
-
 Known differences
 =================
 
diff --git a/package.json b/package.json
index 2d3a5be..7e61222 100644
--- a/package.json
+++ b/package.json
@@ -1,132 +1,114 @@
 {
-    "name": "node-fetch",
-    "version": "3.0.0-beta.5",
-    "description": "A light-weight module that brings window.fetch to node.js",
-    "main": "./dist/index.js",
-    "module": "./src/index.js",
-    "sideEffects": false,
-    "exports": {
-        "import": "./src/index.js",
-        "require": "./dist/index.js"
-    },
-    "files": [
-        "src",
-        "dist",
-        "*.d.ts"
-    ],
-    "engines": {
-        "node": ">=10"
-    },
-    "scripts": {
-        "build": "babel src --out-dir dist",
-        "test": "nyc --reporter=html --reporter=text mocha --require @babel/register --throw-deprecation",
-        "coverage": "nyc report --reporter=text-lcov | coveralls",
-        "lint": "xo"
-    },
-    "repository": {
-        "type": "git",
-        "url": "https://github.com/node-fetch/node-fetch.git"
-    },
-    "keywords": [
-        "fetch",
-        "http",
-        "promise"
-    ],
-    "author": "David Frank",
-    "license": "MIT",
-    "bugs": {
-        "url": "https://github.com/node-fetch/node-fetch/issues"
-    },
-    "homepage": "https://github.com/node-fetch/node-fetch",
-    "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/node-fetch"
-    },
-    "devDependencies": {
-        "@babel/cli": "^7.8.4",
-        "@babel/core": "^7.9.0",
-        "@babel/preset-env": "^7.9.5",
-        "@babel/register": "^7.9.0",
-        "@istanbuljs/nyc-config-babel": "^3.0.0",
-        "abort-controller": "^3.0.0",
-        "abortcontroller-polyfill": "^1.4.0",
-        "babel-plugin-add-module-exports": "^1.0.2",
-        "babel-plugin-istanbul": "^6.0.0",
-        "chai": "^4.2.0",
-        "chai-as-promised": "^7.1.1",
-        "chai-iterator": "^3.0.2",
-        "chai-string": "^1.5.0",
-        "coveralls": "^3.0.13",
-        "form-data": "^3.0.0",
-        "mocha": "^7.1.1",
-        "nyc": "^15.0.1",
-        "parted": "^0.1.1",
-        "promise": "^8.1.0",
-        "resumer": "0.0.0",
-        "string-to-arraybuffer": "^1.0.2",
-        "xo": "^0.29.1"
-    },
-    "dependencies": {
-        "data-uri-to-buffer": "^3.0.0",
-        "fetch-blob": "^1.0.5"
-    },
-    "xo": {
-        "envs": [
-            "node",
-            "browser"
-        ],
-        "rules": {
-            "complexity": 0,
-            "promise/prefer-await-to-then": 0,
-            "no-mixed-operators": 0,
-            "no-negated-condition": 0,
-            "unicorn/prevent-abbreviations": 0,
-            "@typescript-eslint/prefer-readonly-parameter-types": 0
-        },
-        "ignores": [
+	"name": "node-fetch",
+	"version": "3.0.0-beta.5",
+	"description": "A light-weight module that brings window.fetch to node.js",
+	"main": "./dist/index.cjs",
+	"module": "./src/index.js",
+	"sideEffects": false,
+	"type": "module",
+	"exports": {
+		"import": "./src/index.js",
+		"require": "./dist/index.cjs"
+	},
+	"files": [
+		"src",
+		"dist",
+		"*.d.ts"
+	],
+	"engines": {
+		"node": ">=10"
+	},
+	"scripts": {
+		"build": "rollup -c",
+		"test": "node --experimental-modules node_modules/.bin/c8 --reporter=html --reporter=text --check-coverage node --experimental-modules node_modules/.bin/mocha",
+		"coverage": "c8 report --reporter=text-lcov | coveralls",
+		"lint": "xo"
+	},
+	"repository": {
+		"type": "git",
+		"url": "https://github.com/node-fetch/node-fetch.git"
+	},
+	"keywords": [
+		"fetch",
+		"http",
+		"promise"
+	],
+	"author": "David Frank",
+	"license": "MIT",
+	"bugs": {
+		"url": "https://github.com/node-fetch/node-fetch/issues"
+	},
+	"homepage": "https://github.com/node-fetch/node-fetch",
+	"funding": {
+		"type": "opencollective",
+		"url": "https://opencollective.com/node-fetch"
+	},
+	"devDependencies": {
+		"abort-controller": "^3.0.0",
+		"abortcontroller-polyfill": "^1.4.0",
+		"c8": "^7.1.2",
+		"chai": "^4.2.0",
+		"chai-as-promised": "^7.1.1",
+		"chai-iterator": "^3.0.2",
+		"chai-string": "^1.5.0",
+		"coveralls": "^3.1.0",
+		"form-data": "^3.0.0",
+		"mocha": "^7.1.2",
+		"parted": "^0.1.1",
+		"promise": "^8.1.0",
+		"resumer": "0.0.0",
+		"rollup": "^2.10.4",
+		"string-to-arraybuffer": "^1.0.2",
+		"xo": "^0.30.0"
+	},
+	"dependencies": {
+		"data-uri-to-buffer": "^3.0.0",
+		"fetch-blob": "^1.0.5"
+	},
+	"esm": {
+		"sourceMap": true
+	},
+	"xo": {
+		"envs": [
+			"node",
+			"browser"
+		],
+		"rules": {
+			"complexity": 0,
+			"promise/prefer-await-to-then": 0,
+			"no-mixed-operators": 0,
+			"no-negated-condition": 0,
+			"unicorn/prevent-abbreviations": 0,
+			"@typescript-eslint/prefer-readonly-parameter-types": 0,
+			"import/extensions": 0,
+			"import/no-useless-path-segments": 0,
+			"unicorn/import-index": 0
+		},
+		"ignores": [
 			"dist",
 			"index.d.ts"
-        ],
-        "overrides": [
-            {
-                "files": "test/**/*.js",
-                "envs": [
-                    "node",
-                    "mocha"
-                ],
-                "rules": {
-                    "max-nested-callbacks": 0,
-                    "no-unused-expressions": 0,
-                    "new-cap": 0,
-                    "guard-for-in": 0
-                }
-            },
-            {
-                "files": "example.js",
-                "rules": {
-                    "import/no-extraneous-dependencies": 0
-                }
-            }
-        ]
-    },
-    "babel": {
-        "presets": [
-            [
-                "@babel/preset-env",
-                {
-                    "targets": {
-                        "node": true
-                    }
-                }
-            ]
-        ],
-        "plugins": [
-            "add-module-exports",
-            "istanbul"
-        ]
-    },
-    "nyc": {
-        "extends": "@istanbuljs/nyc-config-babel"
-    },
-    "runkitExampleFilename": "example.js"
+		],
+		"overrides": [
+			{
+				"files": "test/**/*.js",
+				"envs": [
+					"node",
+					"mocha"
+				],
+				"rules": {
+					"max-nested-callbacks": 0,
+					"no-unused-expressions": 0,
+					"new-cap": 0,
+					"guard-for-in": 0
+				}
+			},
+			{
+				"files": "example.js",
+				"rules": {
+					"import/no-extraneous-dependencies": 0
+				}
+			}
+		]
+	},
+	"runkitExampleFilename": "example.js"
 }
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..a2a5861
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,18 @@
+import {builtinModules} from 'module';
+import {dependencies} from './package.json';
+
+export default {
+	input: 'src/index.js',
+	output: {
+		file: 'dist/index.cjs',
+		format: 'cjs',
+		esModule: false,
+		interop: false,
+		sourcemap: true,
+		preferConst: true,
+		exports: 'named',
+		// https://github.com/rollup/rollup/issues/1961#issuecomment-534977678
+		intro: 'exports = module.exports = fetch;'
+	},
+	external: [...builtinModules, ...Object.keys(dependencies)]
+};
diff --git a/src/body.js b/src/body.js
index 9d19c89..2c84536 100644
--- a/src/body.js
+++ b/src/body.js
@@ -8,8 +8,8 @@
 import Stream, {PassThrough} from 'stream';
 
 import Blob from 'fetch-blob';
-import FetchError from './errors/fetch-error';
-import {isBlob, isURLSearchParams, isArrayBuffer, isAbortError} from './utils/is';
+import FetchError from './errors/fetch-error.js';
+import {isBlob, isURLSearchParams, isArrayBuffer, isAbortError} from './utils/is.js';
 
 const INTERNALS = Symbol('Body internals');
 
@@ -200,7 +200,9 @@ function consumeBody() {
 		if (this.timeout) {
 			resTimeout = setTimeout(() => {
 				abort = true;
-				reject(new FetchError(`Response timeout while trying to fetch ${this.url} (over ${this.timeout}ms)`, 'body-timeout'));
+				const err = new FetchError(`Response timeout while trying to fetch ${this.url} (over ${this.timeout}ms)`, 'body-timeout');
+				reject(err);
+				body.destroy(err);
 			}, this.timeout);
 		}
 
diff --git a/src/headers.js b/src/headers.js
index f4ab4dd..f767ef1 100644
--- a/src/headers.js
+++ b/src/headers.js
@@ -272,13 +272,17 @@ Object.defineProperties(Headers.prototype, {
 
 function getHeaders(headers, kind = 'key+value') {
 	const keys = Object.keys(headers[MAP]).sort();
-	return keys.map(
-		kind === 'key' ?
-			k => k.toLowerCase() :
-			(kind === 'value' ?
-				k => headers[MAP][k].join(', ') :
-				k => [k.toLowerCase(), headers[MAP][k].join(', ')])
-	);
+
+	let iterator;
+	if (kind === 'key') {
+		iterator = header => header.toLowerCase();
+	} else if (kind === 'value') {
+		iterator = header => headers[MAP][header].join(', ');
+	} else {
+		iterator = header => [header.toLowerCase(), headers[MAP][header].join(', ')];
+	}
+
+	return keys.map(header => iterator(header));
 }
 
 const INTERNAL = Symbol('internal');
diff --git a/src/index.js b/src/index.js
index 3148194..8cac021 100644
--- a/src/index.js
+++ b/src/index.js
@@ -12,12 +12,18 @@ import zlib from 'zlib';
 import Stream, {PassThrough, pipeline as pump} from 'stream';
 import dataURIToBuffer from 'data-uri-to-buffer';
 
-import Body, {writeToStream, getTotalBytes} from './body';
-import Response from './response';
-import Headers, {createHeadersLenient} from './headers';
-import Request, {getNodeRequestOptions} from './request';
-import FetchError from './errors/fetch-error';
-import AbortError from './errors/abort-error';
+import Body, {writeToStream, getTotalBytes} from './body.js';
+import Response from './response.js';
+import Headers, {createHeadersLenient} from './headers.js';
+import Request, {getNodeRequestOptions} from './request.js';
+import FetchError from './errors/fetch-error.js';
+import AbortError from './errors/abort-error.js';
+
+export {default as Headers} from './headers.js';
+export {default as Request} from './request.js';
+export {default as Response} from './response.js';
+export {default as FetchError} from './errors/fetch-error.js';
+export {default as AbortError} from './errors/abort-error.js';
 
 /**
  * Fetch function
@@ -111,6 +117,7 @@ export default function fetch(url, options_) {
 		});
 
 		request_.on('response', res => {
+			request_.setTimeout(0);
 			const headers = createHeadersLenient(res.headers);
 
 			// HTTP fetch step 5
@@ -305,7 +312,3 @@ fetch.isRedirect = code => [301, 302, 303, 307, 308].includes(code);
 
 // Expose Promise
 fetch.Promise = global.Promise;
-fetch.Headers = Headers;
-fetch.Request = Request;
-fetch.Response = Response;
-fetch.FetchError = FetchError;
diff --git a/src/request.js b/src/request.js
index 19b79d3..6ccdec7 100644
--- a/src/request.js
+++ b/src/request.js
@@ -9,10 +9,10 @@
 
 import {format as formatUrl} from 'url';
 import Stream from 'stream';
-import Headers, {exportNodeCompatibleHeaders} from './headers';
-import Body, {clone, extractContentType, getTotalBytes} from './body';
-import {isAbortSignal} from './utils/is';
-import {getSearch} from './utils/get-search';
+import Headers, {exportNodeCompatibleHeaders} from './headers.js';
+import Body, {clone, extractContentType, getTotalBytes} from './body.js';
+import {isAbortSignal} from './utils/is.js';
+import {getSearch} from './utils/get-search.js';
 
 const INTERNALS = Symbol('Request internals');
 
@@ -203,11 +203,6 @@ export function getNodeRequestOptions(request) {
 		headers.set('Accept', '*/*');
 	}
 
-	// Basic fetch
-	if (!parsedURL.protocol || !parsedURL.hostname) {
-		throw new TypeError('Only absolute URLs are supported');
-	}
-
 	if (!/^https?:$/.test(parsedURL.protocol)) {
 		throw new TypeError('Only HTTP(S) protocols are supported');
 	}
diff --git a/src/response.js b/src/response.js
index a7ec567..1fb6aba 100644
--- a/src/response.js
+++ b/src/response.js
@@ -4,8 +4,8 @@
  * Response class provides content decoding
  */
 
-import Headers from './headers';
-import Body, {clone, extractContentType} from './body';
+import Headers from './headers.js';
+import Body, {clone, extractContentType} from './body.js';
 
 const INTERNALS = Symbol('Response internals');
 


Test patch:
diff --git a/test/external-encoding.js b/test/external-encoding.js
index b7a3137..3bfd1d8 100644
--- a/test/external-encoding.js
+++ b/test/external-encoding.js
@@ -1,4 +1,4 @@
-import fetch from '../src';
+import fetch from '../src/index.js';
 import chai from 'chai';
 
 const {expect} = chai;
diff --git a/test/headers.js b/test/headers.js
index ef7b705..ef6973d 100644
--- a/test/headers.js
+++ b/test/headers.js
@@ -1,4 +1,4 @@
-import {Headers} from '../src';
+import {Headers} from '../src/index.js';
 import chai from 'chai';
 
 const {expect} = chai;
diff --git a/test/main.js b/test/main.js
index 81e1273..0185854 100644
--- a/test/main.js
+++ b/test/main.js
@@ -2,10 +2,9 @@
 import zlib from 'zlib';
 import crypto from 'crypto';
 import {spawn} from 'child_process';
-import * as http from 'http';
-import * as fs from 'fs';
-import * as path from 'path';
-import * as stream from 'stream';
+import http from 'http';
+import fs from 'fs';
+import stream from 'stream';
 import {lookup} from 'dns';
 import vm from 'vm';
 import chai from 'chai';
@@ -17,29 +16,33 @@ import resumer from 'resumer';
 import FormData from 'form-data';
 import stringToArrayBuffer from 'string-to-arraybuffer';
 
-import {AbortController} from 'abortcontroller-polyfill/dist/abortcontroller';
+import polyfill from 'abortcontroller-polyfill/dist/abortcontroller.js';
 import AbortController2 from 'abort-controller';
 
+const {AbortController} = polyfill;
+
 // Test subjects
 import Blob from 'fetch-blob';
+
 import fetch, {
 	FetchError,
 	Headers,
 	Request,
 	Response
-} from '../src';
-import FetchErrorOrig from '../src/errors/fetch-error';
-import HeadersOrig, {createHeadersLenient} from '../src/headers';
-import RequestOrig from '../src/request';
-import ResponseOrig from '../src/response';
-import Body, {getTotalBytes, extractContentType} from '../src/body';
-import TestServer from './utils/server';
+} from '../src/index.js';
+import FetchErrorOrig from '../src/errors/fetch-error.js';
+import HeadersOrig, {createHeadersLenient} from '../src/headers.js';
+import RequestOrig from '../src/request.js';
+import ResponseOrig from '../src/response.js';
+import Body, {getTotalBytes, extractContentType} from '../src/body.js';
+import delay from './utils/delay.js';
+import TestServer from './utils/server.js';
 
 const {
 	Uint8Array: VMUint8Array
 } = vm.runInNewContext('this');
 
-import chaiTimeout from './utils/chai-timeout';
+import chaiTimeout from './utils/chai-timeout.js';
 
 chai.use(chaiPromised);
 chai.use(chaiIterator);
@@ -796,16 +799,8 @@ describe('node-fetch', () => {
 	});
 
 	it('should collect handled errors on the body stream to reject if the body is used later', () => {
-		function delay(value) {
-			return new Promise(resolve => {
-				setTimeout(() => {
-					resolve(value);
-				}, 20);
-			});
-		}
-
 		const url = `${base}invalid-content-encoding`;
-		return fetch(url).then(delay).then(res => {
+		return fetch(url).then(delay(20)).then(res => {
 			expect(res.headers.get('content-type')).to.equal('text/plain');
 			return expect(res.text()).to.eventually.be.rejected
 				.and.be.an.instanceOf(FetchError)
@@ -863,6 +858,21 @@ describe('node-fetch', () => {
 		});
 	});
 
+	it('should not allow socket timeout before body is read', () => {
+		const url = `${base}slow`;
+		const options = {
+			timeout: 100
+		};
+		// Await the response, then delay, allowing enough time for the timeout
+		// to be created just before the socket timeout
+		return fetch(url, options).then(delay(75)).then(res => {
+			expect(res.ok).to.be.true;
+			return expect(res.text()).to.eventually.be.rejected
+				.and.be.an.instanceOf(FetchError)
+				.and.have.property('type', 'body-timeout');
+		});
+	});
+
 	it('should allow custom timeout on redirected requests', () => {
 		const url = `${base}redirect/slow-chain`;
 		const options = {
@@ -1378,7 +1388,7 @@ describe('node-fetch', () => {
 
 	itIf(process.platform !== 'win32')('should allow POST request with form-data using stream as body', () => {
 		const form = new FormData();
-		form.append('my_field', fs.createReadStream(path.join(__dirname, './utils/dummy.txt')));
+		form.append('my_field', fs.createReadStream('test/utils/dummy.txt'));
 
 		const url = `${base}multipart`;
 		const options = {
diff --git a/test/request.js b/test/request.js
index c83461b..9b38d5d 100644
--- a/test/request.js
+++ b/test/request.js
@@ -1,14 +1,16 @@
-import * as stream from 'stream';
-import * as http from 'http';
-import {Request} from '../src';
-import TestServer from './utils/server';
-import {AbortController} from 'abortcontroller-polyfill/dist/abortcontroller';
+import stream from 'stream';
+import http from 'http';
+import polyfill from 'abortcontroller-polyfill/dist/abortcontroller.js';
 import chai from 'chai';
 import FormData from 'form-data';
 import Blob from 'fetch-blob';
 import resumer from 'resumer';
 import stringToArrayBuffer from 'string-to-arraybuffer';
 
+import TestServer from './utils/server.js';
+import {Request} from '../src/index.js';
+
+const {AbortController} = polyfill;
 const {expect} = chai;
 
 const local = new TestServer();
@@ -102,17 +104,17 @@ describe('Request', () => {
 	});
 
 	it('should throw error with GET/HEAD requests with body', () => {
-		expect(() => new Request('.', {body: ''}))
+		expect(() => new Request(base, {body: ''}))
 			.to.throw(TypeError);
-		expect(() => new Request('.', {body: 'a'}))
+		expect(() => new Request(base, {body: 'a'}))
 			.to.throw(TypeError);
-		expect(() => new Request('.', {body: '', method: 'HEAD'}))
+		expect(() => new Request(base, {body: '', method: 'HEAD'}))
 			.to.throw(TypeError);
-		expect(() => new Request('.', {body: 'a', method: 'HEAD'}))
+		expect(() => new Request(base, {body: 'a', method: 'HEAD'}))
 			.to.throw(TypeError);
-		expect(() => new Request('.', {body: 'a', method: 'get'}))
+		expect(() => new Request(base, {body: 'a', method: 'get'}))
 			.to.throw(TypeError);
-		expect(() => new Request('.', {body: 'a', method: 'head'}))
+		expect(() => new Request(base, {body: 'a', method: 'head'}))
 			.to.throw(TypeError);
 	});
 
diff --git a/test/response.js b/test/response.js
index 35a8090..1e523b8 100644
--- a/test/response.js
+++ b/test/response.js
@@ -1,10 +1,10 @@
 import * as stream from 'stream';
-import {Response} from '../src';
-import TestServer from './utils/server';
 import chai from 'chai';
 import resumer from 'resumer';
 import stringToArrayBuffer from 'string-to-arraybuffer';
 import Blob from 'fetch-blob';
+import {Response} from '../src/index.js';
+import TestServer from './utils/server.js';
 
 const {expect} = chai;
 
diff --git a/test/utils/delay.js b/test/utils/delay.js
new file mode 100644
index 0000000..8484848
--- /dev/null
+++ b/test/utils/delay.js
@@ -0,0 +1,7 @@
+export default function delay(ms) {
+	return value => new Promise(resolve => {
+		setTimeout(() => {
+			resolve(value);
+		}, ms);
+	});
+}
diff --git a/test/utils/server.js b/test/utils/server.js
index 0616d35..d03d54b 100644
--- a/test/utils/server.js
+++ b/test/utils/server.js
@@ -1,6 +1,7 @@
-import * as http from 'http';
-import * as zlib from 'zlib';
-import {multipart as Multipart} from 'parted';
+import http from 'http';
+import zlib from 'zlib';
+import parted from 'parted';
+const {multipart: Multipart} = parted;
 
 export default class TestServer {
 	constructor() {
@@ -379,9 +380,3 @@ export default class TestServer {
 	}
 }
 
-if (require.main === module) {
-	const server = new TestServer();
-	server.start(() => {
-		console.log(`Server started listening at port ${server.port}`);
-	});
-}