Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Options {
redirect: boolean;
timeout: number;
body: any;
signal: AbortSignal;
}

export interface Response<T = any> {
Expand Down
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "httpie",
"version": "2.0.0-next.13",
"version": "2.0.0-next.14",
"repository": "lukeed/httpie",
"description": "A lightweight, Promise-based HTTP client for Node.js and the browser~!",
"browser": "xhr/index.mjs",
Expand All @@ -12,24 +12,29 @@
"license": "MIT",
"exports": {
".": {
"types": "./index.d.ts",
"worker": "./fetch/index.mjs",
"browser": "./xhr/index.mjs",
"import": "./node/index.mjs",
"require": "./node/index.js"
},
"./node": {
"types": "./node/index.d.ts",
"import": "./node/index.mjs",
"require": "./node/index.js"
},
"./fetch": {
"types": "./fetch/index.d.ts",
"import": "./fetch/index.mjs",
"require": "./fetch/index.js"
},
"./browser": {
"types": "./xhr/index.d.ts",
"import": "./xhr/index.mjs",
"require": "./xhr/index.js"
},
"./xhr": {
"types": "./xhr/index.d.ts",
"import": "./xhr/index.mjs",
"require": "./xhr/index.js"
},
Expand Down Expand Up @@ -69,8 +74,8 @@
"xhr"
],
"devDependencies": {
"bundt": "1.1.2",
"bundt": "1.1.5",
"esm": "3.2.25",
"uvu": "0.5.1"
"uvu": "0.5.6"
}
}
1 change: 1 addition & 0 deletions src/fetch.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface Options extends Partial<RequestInit> {
withCredentials: boolean;
timeout: number;
body: any;
signal: AbortSignal;
}

export function send<T = any>(method: string, uri: URL | string, opts?: Partial<Options>): Promise<Response<T>>;
Expand Down
26 changes: 19 additions & 7 deletions src/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function apply(src, tar) {

export function send(method, uri, opts) {
opts = opts || {};
var timer, ctrl, tmp=opts.body;
var timer, aborted, timedout = false, ctrl, tmp=opts.body;

opts.method = method;
opts.headers = opts.headers || {};
Expand All @@ -23,15 +23,24 @@ export function send(method, uri, opts) {
}

if (opts.timeout) {
ctrl = new AbortController;
opts.signal = ctrl.signal;
timer = setTimeout(ctrl.abort, opts.timeout);
if (!opts.signal) {
ctrl = new AbortController;
opts.signal = ctrl.signal;
}
timer = setTimeout(function() {
timedout = true;
ctrl.signal.dispatchEvent(new Event('abort'));
}, opts.timeout);
}

if (opts.signal) {
opts.signal.addEventListener('abort', function () {
aborted = true;
});
}

return new Promise((res, rej) => {
fetch(uri, opts).then((rr, reply) => {
clearTimeout(timer);

apply(rr, rr); //=> rr.headers
reply = rr.status >= 400 ? rej : res;

Expand All @@ -51,8 +60,11 @@ export function send(method, uri, opts) {
});
}
}).catch(err => {
err.timeout = ctrl && ctrl.signal.aborted;
err.timeout = timedout;
err.aborted = aborted && !timedout;
rej(err);
}).finally(() => {
clearTimeout(timer);
});
});
}
Expand Down
1 change: 1 addition & 0 deletions src/node.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Options {
redirect: boolean;
timeout: number;
body: any;
signal: AbortSignal;
}

export function send<T = any>(method: string, uri: URL | Url | string, opts?: Partial<Options>): Promise<Response<T>>;
Expand Down
19 changes: 15 additions & 4 deletions src/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function toError(rej, res, err) {

export function send(method, uri, opts={}) {
return new Promise((res, rej) => {
let req, tmp, out = '';
let req, tmp, aborted, timedout = false, out = '';
let { redirect=true } = opts;
opts.method = method;

Expand Down Expand Up @@ -49,13 +49,24 @@ export function send(method, uri, opts={}) {
});
});

req.on('timeout', req.abort);
req.on('timeout', function() {
timedout = true;
req.abort();
});

req.on('error', err => {
// Node 11.x ~> boolean, else timestamp
err.timeout = req.aborted;
err.timeout = timedout;
err.aborted = aborted;
rej(err);
});

if (opts.signal) {
opts.signal.addEventListener('abort', function () {
aborted = true;
req.destroy();
});
}

if (opts.body) {
tmp = typeof opts.body === 'object' && !Buffer.isBuffer(opts.body);
tmp && req.setHeader('content-type', 'application/json');
Expand Down
1 change: 1 addition & 0 deletions src/xhr.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface Options {
redirect: boolean;
timeout: number;
body: any;
signal: AbortSignal;
}

export function send<T = any>(method: string, uri: URL | string, opts?: Partial<Options>): Promise<Response<T>>;
Expand Down
13 changes: 12 additions & 1 deletion src/xhr.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ export function send(method, uri, opts) {
req.ontimeout = req.onerror = function (err) {
err.timeout = err.type == 'timeout';
rej(err);
}
};

req.onabort = function (err) {
err.aborted = true;
rej(err);
};

req.open(method, uri.href || uri);

Expand Down Expand Up @@ -57,6 +62,12 @@ export function send(method, uri, opts) {
}

req.send(str);

if (opts.signal) {
opts.signal.addEventListener('abort', function () {
req.abort();
});
}
});
}

Expand Down
48 changes: 32 additions & 16 deletions test/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import * as assert from 'uvu/assert';
import { server, isResponse } from './utils';
import * as httpie from '../src/node';

const reqresHeaders = { 'x-api-key': 'reqres-free-v1' };

test('exports', () => {
assert.type(httpie, 'object');

Expand All @@ -16,7 +18,7 @@ test('exports', () => {
});

test('GET (200)', async () => {
let res = await httpie.get('https://reqres.in/api/users/2');
let res = await httpie.get('https://reqres.in/api/users/2', { headers: { ...reqresHeaders } });
isResponse(res, 200);

let data = res.data;
Expand All @@ -27,7 +29,7 @@ test('GET (200)', async () => {

test('GET (404)', async () => {
try {
await httpie.get('https://reqres.in/api/users/23');
await httpie.get('https://reqres.in/api/users/23', { headers: { ...reqresHeaders } });
assert.unreachable('should have thrown error');
} catch (err) {
assert.instance(err, Error, '~> returns a true Error instance');
Expand All @@ -43,7 +45,7 @@ test('POST (201)', async () => {
job: 'leader'
};

let res = await httpie.post('https://reqres.in/api/users', { body });
let res = await httpie.post('https://reqres.in/api/users', { body, headers: { ...reqresHeaders } });

isResponse(res, 201);
assert.ok(res.data.id, '~~> created item w/ "id" value');
Expand All @@ -58,7 +60,7 @@ test('PUT (200)', async () => {
job: 'zion resident'
};

let res = await httpie.put('https://reqres.in/api/users/2', { body });
let res = await httpie.put('https://reqres.in/api/users/2', { body, headers: { ...reqresHeaders } });

isResponse(res, 200);
assert.is(res.data.job, body.job, '~~> created item w/ "job" value');
Expand All @@ -72,7 +74,7 @@ test('PATCH (200)', async () => {
job: 'rebel'
};

let res = await httpie.patch('https://reqres.in/api/users/2', { body });
let res = await httpie.patch('https://reqres.in/api/users/2', { body, headers: { ...reqresHeaders } });

isResponse(res, 200);
assert.is(res.data.job, body.job, '~~> created item w/ "job" value');
Expand All @@ -81,19 +83,19 @@ test('PATCH (200)', async () => {
});

test('DELETE (204)', async () => {
let res = await httpie.del('https://reqres.in/api/users/2');
let res = await httpie.del('https://reqres.in/api/users/2', { headers: { ...reqresHeaders } });
assert.is(res.statusCode, 204);
assert.is(res.data, '');
});

test('GET (HTTP -> HTTPS)', async () => {
let res = await httpie.get('http://reqres.in/api/users');
let res = await httpie.get('http://reqres.in/api/users', { headers: { ...reqresHeaders } });
assert.is(res.req.agent.protocol, 'https:', '~> follow-up request with HTTPS');
isResponse(res, 200);
});

test('GET (301 = redirect:false)', async () => {
let res = await httpie.get('http://reqres.in/api/users', { redirect:0 });
let res = await httpie.get('http://reqres.in/api/users', { redirect: 0, headers: { ...reqresHeaders } });
assert.is(res.statusCode, 301, '~> statusCode = 301');
assert.is(res.statusMessage, 'Moved Permanently', '~> "Moved Permanently"');
assert.is(res.headers.location, 'https://reqres.in/api/users', '~> has "Location" header');
Expand All @@ -102,7 +104,7 @@ test('GET (301 = redirect:false)', async () => {

test('GET (delay)', async () => {
let now = Date.now();
let res = await httpie.send('GET', 'https://reqres.in/api/users?delay=5');
let res = await httpie.send('GET', 'https://reqres.in/api/users?delay=5', { headers: { ...reqresHeaders } });
assert.is(res.statusCode, 200, '~> res.statusCode = 200');
assert.type(res.data, 'object', '~> res.data is an object');
assert.ok(Date.now() - now >= 5e3, '~> waited at least 5 seconds');
Expand All @@ -111,7 +113,7 @@ test('GET (delay)', async () => {
test('POST (string body w/ object url)', async () => {
const body = 'peter@klaven';
const uri = parse('https://reqres.in/api/login');
await httpie.post(uri, { body }).catch(err => {
await httpie.post(uri, { body, headers: { ...reqresHeaders } }).catch(err => {
assert.is(err.message, 'Bad Request');
isResponse(err, 400, {
error: 'Missing email or username'
Expand All @@ -120,7 +122,7 @@ test('POST (string body w/ object url)', async () => {
});

test('custom headers', async () => {
let headers = { 'X-FOO': 'BAR123' };
let headers = { 'X-FOO': 'BAR123', ...reqresHeaders };
let res = await httpie.get('https://reqres.in/api/users', { headers });
let sent = res.req.getHeader('x-foo');

Expand All @@ -134,7 +136,7 @@ function reviver(key, val) {
}

test('GET (reviver)', async () => {
let res = await httpie.get('https://reqres.in/api/users', { reviver });
let res = await httpie.get('https://reqres.in/api/users', { reviver, headers: { ...reqresHeaders } });
assert.is(res.statusCode, 200, '~> statusCode = 200');

assert.is(res.data.per_page, undefined, '~> removed "per_page" key');
Expand All @@ -144,7 +146,7 @@ test('GET (reviver)', async () => {
});

test('GET (reviver w/ redirect)', async () => {
let res = await httpie.get('http://reqres.in/api/users', { reviver });
let res = await httpie.get('http://reqres.in/api/users', { reviver, headers: { ...reqresHeaders } });
assert.is(res.req.agent.protocol, 'https:', '~> follow-up request with HTTPS');
assert.is(res.statusCode, 200, '~> statusCode = 200');

Expand All @@ -156,12 +158,26 @@ test('GET (reviver w/ redirect)', async () => {

test('via Url (legacy)', async () => {
let foo = parse('https://reqres.in/api/users/2');
isResponse(await httpie.get(foo), 200);
isResponse(await httpie.get(foo, { headers: { ...reqresHeaders } }), 200);
});

test('via URL (WHATWG)', async () => {
let foo = new URL('https://reqres.in/api/users/2');
isResponse(await httpie.get(foo), 200);
isResponse(await httpie.get(foo, { headers: { ...reqresHeaders } }), 200);
});

test('Error: abort', async () => {
let ctrl = new AbortController();
let ctx = await server();

setInterval(() => ctrl.abort(), 0); // abort immediately

await httpie.get(`http://localhost:${ctx.port}/any`, { signal: ctrl.signal }).catch(err => {
assert.ok(err.message.includes('operation was aborted'), '~> had "operation was aborted" message');
assert.is(err.timeout, false, '~> timeout = false');
assert.is(err.aborted, true, '~> aborted = true');
ctx.close();
});
});

test('Error: Invalid JSON', async () => {
Expand All @@ -181,7 +197,7 @@ test('Error: Invalid JSON', async () => {
});

test('Error: timeout', async () => {
await httpie.send('GET', 'https://reqres.in/api/users?delay=3', { timeout:1000 }).catch(err => {
await httpie.send('GET', 'https://reqres.in/api/users?delay=3', { timeout: 1000, headers: { ...reqresHeaders } }).catch(err => {
assert.instance(err, Error, '~> caught Error');
assert.is(err.message, 'socket hang up', '~> had "socket hang up" message');
assert.ok(err.timeout !== void 0, '~> added `timeout` property');
Expand Down
Loading