Skip to content

Commit fda6ad3

Browse files
authored
feat: Implement 103 Early Hints (#1217)
1 parent 8dfefad commit fda6ad3

File tree

11 files changed

+350
-57
lines changed

11 files changed

+350
-57
lines changed

documentation/docs/globals/FetchEvent/FetchEvent.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,7 @@ It provides the [`event.respondWith()`](./prototype/respondWith.mdx) method, whi
4040

4141
- [`FetchEvent.respondWith()`](./prototype/respondWith.mdx)
4242
- : Provide (a promise for) a response for this request.
43+
- [`FetchEvent.sendEarlyHint()`](./prototype/sendEarlyHint.mdx)
44+
- : Send a [103 Early Hints](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/103) response for this request.
4345
- [`FetchEvent.waitUntil()`](./prototype/waitUntil.mdx)
4446
- : Extends the lifetime of the event. Used to notify the host environment of tasks that extend beyond the returning of a response, such as streaming and caching.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
hide_title: false
3+
hide_table_of_contents: false
4+
pagination_next: null
5+
pagination_prev: null
6+
---
7+
# FetchEvent.sendEarlyHints()
8+
9+
The **`sendEarlyHints()`** method allows you to send a [103 Early Hints](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/103) response back to the client which made the incoming request to your application.
10+
11+
## Syntax
12+
13+
```js
14+
sendEarlyHints(headers)
15+
```
16+
17+
### Parameters
18+
19+
- `headers`
20+
- : Any headers you want to add to your response, contained
21+
within a [`Headers`](../../globals/Headers/Headers.mdx) object or object literal of
22+
[`String`](../../globals/String/String.mdx) key/value pairs.
23+
24+
### Return value
25+
26+
Always returns `undefined`.
27+
28+
### Examples
29+
30+
```js
31+
event.sendEarlyHints({ link: '</style.css>; rel=preload; as=style' });
32+
33+
event.sendEarlyHints([
34+
['link', '</style.css>; rel=preload; as=style'],
35+
['link', '</style2.css>; rel=preload; as=style']
36+
]);
37+
```
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { pass, ok, strictEqual, assertThrows } from './assertions.js';
2+
import { routes } from './routes.js';
3+
4+
routes.set('/early-hints/manual-response', (event) => {
5+
event.respondWith(
6+
new Response(null, {
7+
status: 103,
8+
headers: { link: '</style.css>; rel=preload; as=style' },
9+
}),
10+
);
11+
event.respondWith(new Response('ok'));
12+
});
13+
14+
routes.set('/early-hints/send-early-hints', (event) => {
15+
event.sendEarlyHints({ link: '</style.css>; rel=preload; as=style' });
16+
event.respondWith(new Response('ok'));
17+
});
18+
19+
routes.set('/early-hints/send-early-hints-multiple-headers', (event) => {
20+
event.sendEarlyHints([
21+
['link', '</style.css>; rel=preload; as=style'],
22+
['link', '</style2.css>; rel=preload; as=style'],
23+
]);
24+
event.respondWith(new Response('ok'));
25+
});

integration-tests/js-compute/fixtures/app/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import './config-store.js';
1818
import './crypto.js';
1919
import './device.js';
2020
import './dictionary.js';
21+
import './early-hints.js';
2122
import './edge-rate-limiter.js';
2223
import './env.js';
2324
import './fanout.js';

integration-tests/js-compute/fixtures/app/tests.json

Lines changed: 167 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -40,32 +40,72 @@
4040
"GET /cache-override/fetch/mode-pass": {
4141
"flake": true
4242
},
43-
"GET /secret-store/exposed-as-global": { "flake": true },
44-
"GET /secret-store/interface": { "flake": true },
45-
"GET /secret-store/constructor/called-as-regular-function": { "flake": true },
43+
"GET /secret-store/exposed-as-global": {
44+
"flake": true
45+
},
46+
"GET /secret-store/interface": {
47+
"flake": true
48+
},
49+
"GET /secret-store/constructor/called-as-regular-function": {
50+
"flake": true
51+
},
4652
"GET /secret-store/constructor/parameter-calls-7.1.17-ToString": {
4753
"flake": true
4854
},
49-
"GET /secret-store/constructor/empty-parameter": { "flake": true },
50-
"GET /secret-store/constructor/found-store": { "flake": true },
51-
"GET /secret-store/constructor/missing-store": { "flake": true },
52-
"GET /secret-store/constructor/invalid-name": { "flake": true },
53-
"GET /secret-store/get/called-as-constructor": { "flake": true },
54-
"GET /secret-store/get/called-unbound": { "flake": true },
55+
"GET /secret-store/constructor/empty-parameter": {
56+
"flake": true
57+
},
58+
"GET /secret-store/constructor/found-store": {
59+
"flake": true
60+
},
61+
"GET /secret-store/constructor/missing-store": {
62+
"flake": true
63+
},
64+
"GET /secret-store/constructor/invalid-name": {
65+
"flake": true
66+
},
67+
"GET /secret-store/get/called-as-constructor": {
68+
"flake": true
69+
},
70+
"GET /secret-store/get/called-unbound": {
71+
"flake": true
72+
},
5573
"GET /secret-store/get/key-parameter-calls-7.1.17-ToString": {
5674
"flake": true
5775
},
58-
"GET /secret-store/get/key-parameter-not-supplied": { "flake": true },
59-
"GET /secret-store/get/key-parameter-empty-string": { "flake": true },
60-
"GET /secret-store/get/key-parameter-255-character-string": { "flake": true },
61-
"GET /secret-store/get/key-parameter-256-character-string": { "flake": true },
62-
"GET /secret-store/get/key-parameter-invalid-string": { "flake": true },
63-
"GET /secret-store/get/key-does-not-exist-returns-null": { "flake": true },
64-
"GET /secret-store/get/key-exists": { "flake": true },
65-
"GET /secret-store/from-bytes/invalid": { "flake": true },
66-
"GET /secret-store/from-bytes/valid": { "flake": true },
67-
"GET /secret-store-entry/interface": { "flake": true },
68-
"GET /secret-store-entry/plaintext": { "flake": true },
76+
"GET /secret-store/get/key-parameter-not-supplied": {
77+
"flake": true
78+
},
79+
"GET /secret-store/get/key-parameter-empty-string": {
80+
"flake": true
81+
},
82+
"GET /secret-store/get/key-parameter-255-character-string": {
83+
"flake": true
84+
},
85+
"GET /secret-store/get/key-parameter-256-character-string": {
86+
"flake": true
87+
},
88+
"GET /secret-store/get/key-parameter-invalid-string": {
89+
"flake": true
90+
},
91+
"GET /secret-store/get/key-does-not-exist-returns-null": {
92+
"flake": true
93+
},
94+
"GET /secret-store/get/key-exists": {
95+
"flake": true
96+
},
97+
"GET /secret-store/from-bytes/invalid": {
98+
"flake": true
99+
},
100+
"GET /secret-store/from-bytes/valid": {
101+
"flake": true
102+
},
103+
"GET /secret-store-entry/interface": {
104+
"flake": true
105+
},
106+
"GET /secret-store-entry/plaintext": {
107+
"flake": true
108+
},
69109
"GET /simple-cache/interface": {},
70110
"GET /simple-store/constructor/called-as-regular-function": {
71111
"flake": true,
@@ -409,7 +449,9 @@
409449
"GET /client/tlsClientCertificate": {},
410450
"GET /client/tlsCipherOpensslName": {},
411451
"GET /client/tlsProtocol": {},
412-
"GET /config-store": { "flake": true },
452+
"GET /config-store": {
453+
"flake": true
454+
},
413455
"GET /crypto": {
414456
"downstream_response": {
415457
"status": 200,
@@ -980,24 +1022,54 @@
9801022
"body": "ok"
9811023
}
9821024
},
983-
"GET /dictionary/exposed-as-global": { "flake": true },
984-
"GET /dictionary/interface": { "flake": true },
985-
"GET /dictionary/constructor/called-as-regular-function": { "flake": true },
1025+
"GET /dictionary/exposed-as-global": {
1026+
"flake": true
1027+
},
1028+
"GET /dictionary/interface": {
1029+
"flake": true
1030+
},
1031+
"GET /dictionary/constructor/called-as-regular-function": {
1032+
"flake": true
1033+
},
9861034
"GET /dictionary/constructor/parameter-calls-7.1.17-ToString": {
9871035
"flake": true
9881036
},
989-
"GET /dictionary/constructor/empty-parameter": { "flake": true },
990-
"GET /dictionary/constructor/found": { "flake": true },
991-
"GET /dictionary/constructor/invalid-name": { "flake": true },
992-
"GET /dictionary/get/called-as-constructor": { "flake": true },
993-
"GET /dictionary/get/called-unbound": { "flake": true },
994-
"GET /dictionary/get/key-parameter-calls-7.1.17-ToString": { "flake": true },
995-
"GET /dictionary/get/key-parameter-not-supplied": { "flake": true },
996-
"GET /dictionary/get/key-parameter-empty-string": { "flake": true },
997-
"GET /dictionary/get/key-parameter-255-character-string": { "flake": true },
998-
"GET /dictionary/get/key-parameter-256-character-string": { "flake": true },
999-
"GET /dictionary/get/key-does-not-exist-returns-null": { "flake": true },
1000-
"GET /dictionary/get/key-exists": { "flake": true },
1037+
"GET /dictionary/constructor/empty-parameter": {
1038+
"flake": true
1039+
},
1040+
"GET /dictionary/constructor/found": {
1041+
"flake": true
1042+
},
1043+
"GET /dictionary/constructor/invalid-name": {
1044+
"flake": true
1045+
},
1046+
"GET /dictionary/get/called-as-constructor": {
1047+
"flake": true
1048+
},
1049+
"GET /dictionary/get/called-unbound": {
1050+
"flake": true
1051+
},
1052+
"GET /dictionary/get/key-parameter-calls-7.1.17-ToString": {
1053+
"flake": true
1054+
},
1055+
"GET /dictionary/get/key-parameter-not-supplied": {
1056+
"flake": true
1057+
},
1058+
"GET /dictionary/get/key-parameter-empty-string": {
1059+
"flake": true
1060+
},
1061+
"GET /dictionary/get/key-parameter-255-character-string": {
1062+
"flake": true
1063+
},
1064+
"GET /dictionary/get/key-parameter-256-character-string": {
1065+
"flake": true
1066+
},
1067+
"GET /dictionary/get/key-does-not-exist-returns-null": {
1068+
"flake": true
1069+
},
1070+
"GET /dictionary/get/key-exists": {
1071+
"flake": true
1072+
},
10011073
"GET /env": {
10021074
"environments": ["viceroy"]
10031075
},
@@ -1148,7 +1220,9 @@
11481220
"flake": true,
11491221
"downstream_response": {
11501222
"status": 200,
1151-
"headers": { "content-type": "text/html" },
1223+
"headers": {
1224+
"content-type": "text/html"
1225+
},
11521226
"body": ["<h1>blob</h1>"]
11531227
}
11541228
},
@@ -1206,7 +1280,9 @@
12061280
"body": ["START\nEND\n"]
12071281
}
12081282
},
1209-
"GET /setTimeout/fetch-timeout": { "flake": true },
1283+
"GET /setTimeout/fetch-timeout": {
1284+
"flake": true
1285+
},
12101286
"GET /clearInterval/exposed-as-global": {},
12111287
"GET /clearInterval/interface": {},
12121288
"GET /clearInterval/called-as-constructor-function": {},
@@ -1318,7 +1394,9 @@
13181394
"downstream_response": {
13191395
"status": 200,
13201396
"body": "meow",
1321-
"headers": { "content-length": "4" }
1397+
"headers": {
1398+
"content-length": "4"
1399+
}
13221400
}
13231401
},
13241402
"GET /override-content-length/response/init/object-literal/false": {
@@ -1333,7 +1411,9 @@
13331411
"downstream_response": {
13341412
"status": 200,
13351413
"body": "meow",
1336-
"headers": { "content-length": "4" }
1414+
"headers": {
1415+
"content-length": "4"
1416+
}
13371417
}
13381418
},
13391419
"GET /override-content-length/response/init/response-instance/false": {
@@ -1350,7 +1430,9 @@
13501430
"environments": ["compute"],
13511431
"downstream_response": {
13521432
"status": 200,
1353-
"headers": { "content-length": "11" }
1433+
"headers": {
1434+
"content-length": "11"
1435+
}
13541436
}
13551437
},
13561438
"GET /headers/getsetcookie": {
@@ -1374,7 +1456,9 @@
13741456
"headers": [["cuStom", "test"]]
13751457
}
13761458
},
1377-
"GET /headers/from-response/delete-invalid": { "flake": true },
1459+
"GET /headers/from-response/delete-invalid": {
1460+
"flake": true
1461+
},
13781462
"GET /headers/from-response/set-delete": {
13791463
"flake": true,
13801464
"downstream_response": {
@@ -3007,5 +3091,45 @@
30073091
"GET /html-rewriter/throw-in-handler": {},
30083092
"GET /html-rewriter/invalid-html": {},
30093093
"GET /html-rewriter/insertion-order": {},
3010-
"GET /html-rewriter/escape-html": {}
3094+
"GET /html-rewriter/escape-html": {},
3095+
"GET /early-hints/manual-response": {
3096+
"environments": ["compute"],
3097+
"downstream_response": {
3098+
"body": "ok",
3099+
"status": 200
3100+
},
3101+
"downstream_info": {
3102+
"status": 103,
3103+
"headers": {
3104+
"link": "</style.css>; rel=preload; as=style"
3105+
}
3106+
}
3107+
},
3108+
"GET /early-hints/send-early-hints": {
3109+
"environments": ["compute"],
3110+
"downstream_response": {
3111+
"body": "ok",
3112+
"status": 200
3113+
},
3114+
"downstream_info": {
3115+
"status": 103,
3116+
"headers": {
3117+
"link": "</style.css>; rel=preload; as=style"
3118+
}
3119+
}
3120+
},
3121+
"GET /early-hints/send-early-hints-multiple-headers": {
3122+
"environments": ["compute"],
3123+
"downstream_response": {
3124+
"body": "ok",
3125+
"status": 200
3126+
},
3127+
"downstream_info": {
3128+
"status": 103,
3129+
"headers": [
3130+
["link", "</style.css>; rel=preload; as=style"],
3131+
["link", "</style2.css>; rel=preload; as=style"]
3132+
]
3133+
}
3134+
}
30113135
}

0 commit comments

Comments
 (0)