Skip to content

Commit

Permalink
Introduce bun --fetch-preconnect <url> ./my-script.ts (oven-sh#12698)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jarred-Sumner authored Jul 22, 2024
1 parent bbf2f5d commit 1a6ead6
Show file tree
Hide file tree
Showing 16 changed files with 613 additions and 31 deletions.
240 changes: 240 additions & 0 deletions docs/api/fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
Bun implements the WHATWG `fetch` standard, with some extensions to meet the needs of server-side JavaScript.

Bun also implements `node:http`, but `fetch` is generally recommended instead.

## Sending an HTTP request

To send an HTTP request, use `fetch`

```ts
const response = await fetch("http://example.com");

console.log(response.status); // => 200

const text = await response.text(); // or response.json(), response.formData(), etc.
```

`fetch` also works with HTTPS URLs.

```ts
const response = await fetch("https://example.com");
```

You can also pass `fetch` a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object.

```ts
const request = new Request("http://example.com", {
method: "POST",
body: "Hello, world!",
});

const response = await fetch(request);
```

### Sending a POST request

To send a POST request, pass an object with the `method` property set to `"POST"`.

```ts
const response = await fetch("http://example.com", {
method: "POST",
body: "Hello, world!",
});
```

`body` can be a string, a `FormData` object, an `ArrayBuffer`, a `Blob`, and more. See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Body/body) for more information.

### Proxying requests

To proxy a request, pass an object with the `proxy` property set to a URL.

```ts
const response = await fetch("http://example.com", {
proxy: "http://proxy.com",
});
```

### Custom headers

To set custom headers, pass an object with the `headers` property set to an object.

```ts
const response = await fetch("http://example.com", {
headers: {
"X-Custom-Header": "value",
},
});
```

You can also set headers using the [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object.

```ts
const headers = new Headers();
headers.append("X-Custom-Header", "value");

const response = await fetch("http://example.com", {
headers,
});
```

### Response bodies

To read the response body, use one of the following methods:

- `response.text(): Promise<string>`: Returns a promise that resolves with the response body as a string.
- `response.json(): Promise<any>`: Returns a promise that resolves with the response body as a JSON object.
- `response.formData(): Promise<FormData>`: Returns a promise that resolves with the response body as a `FormData` object.
- `response.bytes(): Promise<Uint8Array>`: Returns a promise that resolves with the response body as a `Uint8Array`.
- `response.arrayBuffer(): Promise<ArrayBuffer>`: Returns a promise that resolves with the response body as an `ArrayBuffer`.
- `response.blob(): Promise<Blob>`: Returns a promise that resolves with the response body as a `Blob`.

#### Streaming response bodies

You can use async iterators to stream the response body.

```ts
const response = await fetch("http://example.com");

for await (const chunk of response.body) {
console.log(chunk);
}
```

You can also more directly access the `ReadableStream` object.

```ts
const response = await fetch("http://example.com");

const stream = response.body;

const reader = stream.getReader();
const { value, done } = await reader.read();
```

### Fetching a URL with a timeout

To fetch a URL with a timeout, use `AbortSignal.timeout`:

```ts
const response = await fetch("http://example.com", {
signal: AbortSignal.timeout(1000),
});
```

#### Canceling a request

To cancel a request, use an `AbortController`:

```ts
const controller = new AbortController();

const response = await fetch("http://example.com", {
signal: controller.signal,
});

controller.abort();
```

## Debugging

To help with debugging, you can pass `verbose: true` to `fetch`:

```ts
const response = await fetch("http://example.com", {
verbose: true,
});
```

This will print the request and response headers to your terminal:

```sh
[fetch] > HTTP/1.1 GET http://example.com/
[fetch] > Connection: keep-alive
[fetch] > User-Agent: Bun/1.1.21
[fetch] > Accept: */*
[fetch] > Host: example.com
[fetch] > Accept-Encoding: gzip, deflate, br

[fetch] < 200 OK
[fetch] < Content-Encoding: gzip
[fetch] < Age: 201555
[fetch] < Cache-Control: max-age=604800
[fetch] < Content-Type: text/html; charset=UTF-8
[fetch] < Date: Sun, 21 Jul 2024 02:41:14 GMT
[fetch] < Etag: "3147526947+gzip"
[fetch] < Expires: Sun, 28 Jul 2024 02:41:14 GMT
[fetch] < Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
[fetch] < Server: ECAcc (sac/254F)
[fetch] < Vary: Accept-Encoding
[fetch] < X-Cache: HIT
[fetch] < Content-Length: 648
```

Note: `verbose: boolean` is not part of the Web standard `fetch` API and is specific to Bun.

## Performance

Before an HTTP request can be sent, the DNS lookup must be performed. This can take a significant amount of time, especially if the DNS server is slow or the network connection is poor.

After the DNS lookup, the TCP socket must be connected and the TLS handshake might need to be performed. This can also take a significant amount of time.

After the request completes, consuming the response body can also take a significant amount of time and memory.

At every step of the way, Bun provides APIs to help you optimize the performance of your application.

### DNS prefetching

To prefetch a DNS entry, you can use the `dns.prefetch` API. This API is useful when you know you'll need to connect to a host soon and want to avoid the initial DNS lookup.

```ts
import { dns } from "bun";

dns.prefetch("bun.sh", 443);
```

### Preconnect to a host

To preconnect to a host, you can use the `http.prefetch` API. This API is useful when you know you'll need to connect to a host soon and want to start the initial DNS lookup, TCP socket connection, and TLS handshake early.

```ts
import { fetch } from "bun";

fetch.preconnect("https://bun.sh");
```

Note: calling `fetch` immediately after `fetch.preconnect` will not make your request faster. Preconnecting only helps if you know you'll need to connect to a host soon, but you're not ready to make the request yet.

#### Preconnect at startup

To preconnect to a host at startup, you can pass `--fetch-preconnect`:

```sh
$ bun --fetch-preconnect https://bun.sh ./my-script.ts
```

This is sort of like `<link rel="preconnect">` in HTML.

This feature is not implemented on Windows yet. If you're interested in using this feature on Windows, please file an issue and we can implement support for it on Windows.

### Connection pooling & HTTP keep-alive

Bun automatically reuses connections to the same host. This is known as connection pooling. This can significantly reduce the time it takes to establish a connection. You don't need to do anything to enable this; it's automatic.

### Response buffering

Bun goes to great lengths to optimize the performance of reading the response body. The fastest way to read the response body is to use one of these methods:

- `response.text(): Promise<string>`
- `response.json(): Promise<any>`
- `response.formData(): Promise<FormData>`
- `response.bytes(): Promise<Uint8Array>`
- `response.arrayBuffer(): Promise<ArrayBuffer>`
- `response.blob(): Promise<Blob>`

You can also use `Bun.write` to write the response body to a file on disk:

```ts
import { write } from "bun";

await write("output.txt", response);
```
5 changes: 4 additions & 1 deletion docs/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,11 @@ export default {

divider("API"),
page("api/http", "HTTP server", {
description: `Bun implements Web-standard fetch, plus a Bun-native API for building fast HTTP servers.`,
description: `Bun implements a fast HTTP server built on Request/Response objects, along with supporting node:http APIs.`,
}), // "`Bun.serve`"),
page("api/fetch", "HTTP client", {
description: `Bun implements Web-standard fetch with some Bun-native extensions.`,
}), // "fetch"),
page("api/websockets", "WebSockets", {
description: `Bun supports server-side WebSockets with on-the-fly compression, TLS support, and a Bun-native pubsub API.`,
}), // "`Bun.serve`"),
Expand Down
4 changes: 4 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3100,6 +3100,10 @@ declare module "bun" {
*/
function openInEditor(path: string, options?: EditorOptions): void;

const fetch: typeof globalThis.fetch & {
preconnect(url: string): void;
};

interface EditorOptions {
editor?: "vscode" | "subl";
line?: number;
Expand Down
54 changes: 35 additions & 19 deletions packages/bun-types/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -907,26 +907,42 @@ declare global {
new (): ShadowRealm;
};

/**
* Send a HTTP(s) request
*
* @param request Request object
* @param init A structured value that contains settings for the fetch() request.
*
* @returns A promise that resolves to {@link Response} object.
*/
interface Fetch {
/**
* Send a HTTP(s) request
*
* @param request Request object
* @param init A structured value that contains settings for the fetch() request.
*
* @returns A promise that resolves to {@link Response} object.
*/
(request: Request, init?: RequestInit): Promise<Response>;

// tslint:disable-next-line:unified-signatures
function fetch(request: Request, init?: RequestInit): Promise<Response>;
/**
* Send a HTTP(s) request
*
* @param url URL string
* @param init A structured value that contains settings for the fetch() request.
*
* @returns A promise that resolves to {@link Response} object.
*/
function fetch(url: string | URL | Request, init?: FetchRequestInit): Promise<Response>;
/**
* Send a HTTP(s) request
*
* @param url URL string
* @param init A structured value that contains settings for the fetch() request.
*
* @returns A promise that resolves to {@link Response} object.
*/
(url: string | URL | Request, init?: FetchRequestInit): Promise<Response>;

(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>;

/**
* Start the DNS resolution, TCP connection, and TLS handshake for a request
* before the request is actually sent.
*
* This can reduce the latency of a request when you know there's some
* long-running task that will delay the request starting.
*
* This is a bun-specific API and is not part of the Fetch API specification.
*/
preconnect(url: string | URL): void;
}

var fetch: Fetch;

function queueMicrotask(callback: (...args: any[]) => void): void;
/**
Expand Down
20 changes: 16 additions & 4 deletions src/bun.js/bindings/BunObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__lookupService);
BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__prefetch);
BUN_DECLARE_HOST_FUNCTION(Bun__DNSResolver__getCacheStats);
BUN_DECLARE_HOST_FUNCTION(Bun__fetch);
BUN_DECLARE_HOST_FUNCTION(Bun__fetchPreconnect);

namespace Bun {

Expand Down Expand Up @@ -267,6 +268,17 @@ static JSValue constructPasswordObject(VM& vm, JSObject* bunObject)
return JSValue::decode(JSPasswordObject__create(bunObject->globalObject()));
}

JSValue constructBunFetchObject(VM& vm, JSObject* bunObject)
{
JSFunction* fetchFn = JSFunction::create(vm, bunObject->globalObject(), 1, "fetch"_s, Bun__fetch, ImplementationVisibility::Public, NoIntrinsic);

auto* globalObject = jsCast<Zig::GlobalObject*>(bunObject->globalObject());
fetchFn->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "preconnect"_s), 1, Bun__fetchPreconnect, ImplementationVisibility::Public, NoIntrinsic,
JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0);

return fetchFn;
}

static JSValue constructBunShell(VM& vm, JSObject* bunObject)
{
auto* globalObject = jsCast<Zig::GlobalObject*>(bunObject->globalObject());
Expand Down Expand Up @@ -549,14 +561,14 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
cwd BunObject_getter_wrap_cwd DontEnum|DontDelete|PropertyCallback
deepEquals functionBunDeepEquals DontDelete|Function 2
deepMatch functionBunDeepMatch DontDelete|Function 2
deflateSync BunObject_callback_deflateSync DontDelete|Function 1
deflateSync BunObject_callback_deflateSync DontDelete|Function 1
dns constructDNSObject ReadOnly|DontDelete|PropertyCallback
enableANSIColors BunObject_getter_wrap_enableANSIColors DontDelete|PropertyCallback
env constructEnvObject ReadOnly|DontDelete|PropertyCallback
escapeHTML functionBunEscapeHTML DontDelete|Function 2
fetch Bun__fetch ReadOnly|DontDelete|Function 1
file BunObject_callback_file DontDelete|Function 1
fileURLToPath functionFileURLToPath DontDelete|Function 1
fetch constructBunFetchObject ReadOnly|DontDelete|PropertyCallback
file BunObject_callback_file DontDelete|Function 1
fileURLToPath functionFileURLToPath DontDelete|Function 1
gc BunObject_callback_gc DontDelete|Function 1
generateHeapSnapshot BunObject_callback_generateHeapSnapshot DontDelete|Function 1
gunzipSync BunObject_callback_gunzipSync DontDelete|Function 1
Expand Down
2 changes: 2 additions & 0 deletions src/bun.js/bindings/BunObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ JSC_DECLARE_HOST_FUNCTION(functionBunNanoseconds);
JSC_DECLARE_HOST_FUNCTION(functionPathToFileURL);
JSC_DECLARE_HOST_FUNCTION(functionFileURLToPath);

JSC::JSValue constructBunFetchObject(VM& vm, JSObject* bunObject);
JSC::JSObject* createBunObject(VM& vm, JSObject* globalObject);

}
1 change: 0 additions & 1 deletion src/bun.js/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@
using namespace Bun;

BUN_DECLARE_HOST_FUNCTION(Bun__NodeUtil__jsParseArgs);
BUN_DECLARE_HOST_FUNCTION(Bun__fetch);
BUN_DECLARE_HOST_FUNCTION(BUN__HTTP2__getUnpackedSettings);
BUN_DECLARE_HOST_FUNCTION(BUN__HTTP2_getPackedSettings);

Expand Down
2 changes: 1 addition & 1 deletion src/bun.js/bindings/ZigGlobalObject.lut.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
clearTimeout functionClearTimeout Function 1
confirm WebCore__confirm Function 1
dispatchEvent jsFunctionDispatchEvent Function 1
fetch Bun__fetch Function 2
fetch constructBunFetchObject PropertyCallback
postMessage jsFunctionPostMessage Function 1
prompt WebCore__prompt Function 1
queueMicrotask functionQueueMicrotask Function 2
Expand Down
Loading

0 comments on commit 1a6ead6

Please sign in to comment.