Skip to content

Commit 43ddf6e

Browse files
authoredDec 21, 2022
Wire in TCK, finish implementing header kind, and fix some bugs (#11)
1 parent 9d3533e commit 43ddf6e

File tree

5 files changed

+737
-60
lines changed

5 files changed

+737
-60
lines changed
 

‎packages/express/index.ts

Lines changed: 156 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,51 @@ class RequestState {
7979
this._response = response;
8080
}
8181

82+
public headers(kind: HeaderKind): NodeJS.Dict<string | string[] | number> {
83+
switch (kind) {
84+
case HeaderKind.REQUEST:
85+
return this._request.headers;
86+
case HeaderKind.RESPONSE:
87+
return this._response.getHeaders();
88+
case HeaderKind.REQUEST_TRAILERS:
89+
return this._request.trailers;
90+
case HeaderKind.RESPONSE_TRAILERS:
91+
return (this._response as BufferedResponse).buffer.trailers;
92+
}
93+
}
94+
95+
public setHeader(kind: HeaderKind, key: string, value: string[]) {
96+
switch (kind) {
97+
case HeaderKind.REQUEST:
98+
this._request.headers[key] = value;
99+
break;
100+
case HeaderKind.RESPONSE:
101+
this._response.setHeader(key, value);
102+
break;
103+
case HeaderKind.REQUEST_TRAILERS:
104+
this._request.trailers[key] = value[0];
105+
break;
106+
case HeaderKind.RESPONSE_TRAILERS:
107+
(this._response as BufferedResponse).buffer.trailers[key] = value;
108+
}
109+
}
110+
111+
public removeHeader(kind: HeaderKind, key: string) {
112+
switch (kind) {
113+
case HeaderKind.REQUEST:
114+
delete this._request.headers[key];
115+
break;
116+
case HeaderKind.RESPONSE:
117+
this._response.removeHeader(key);
118+
break;
119+
case HeaderKind.REQUEST_TRAILERS:
120+
delete this._request.trailers[key];
121+
break;
122+
case HeaderKind.RESPONSE_TRAILERS:
123+
delete (this._response as BufferedResponse).buffer.trailers[key];
124+
}
125+
}
126+
82127
public get nextCalled(): boolean {
83128
return this._nextCalled;
84129
}
@@ -217,7 +262,7 @@ class HttpHandler {
217262

218263
// Circumvent null checking with !, setMemory must be called before
219264
// host functions.
220-
private memory!: Uint8Array;
265+
private memory!: WebAssembly.Memory;
221266

222267
public getImport() {
223268
return {
@@ -232,7 +277,9 @@ class HttpHandler {
232277
log: this.log.bind(this),
233278
log_enabled: this.logEnabled.bind(this),
234279
read_body: this.readBody.bind(this),
280+
add_header_value: this.addHeader.bind(this),
235281
set_header_value: this.setHeader.bind(this),
282+
remove_header: this.removeHeader.bind(this),
236283
set_method: this.setMethod.bind(this),
237284
set_status_code: this.setStatusCode.bind(this),
238285
set_uri: this.setUri.bind(this),
@@ -241,7 +288,7 @@ class HttpHandler {
241288
}
242289

243290
public setMemory(memory: WebAssembly.Memory) {
244-
this.memory = new Uint8Array(memory.buffer);
291+
this.memory = memory;
245292
}
246293

247294
private enableFeatures(features: number): number {
@@ -265,20 +312,7 @@ class HttpHandler {
265312
bufLimit: number,
266313
): bigint {
267314
const state = stateStorage.getStore()!;
268-
let headers: NodeJS.Dict<string | string[] | number>;
269-
switch (kind) {
270-
case HeaderKind.REQUEST:
271-
headers = state.request.headers;
272-
break;
273-
case HeaderKind.RESPONSE:
274-
headers = state.response.getHeaders();
275-
break;
276-
case HeaderKind.REQUEST_TRAILERS:
277-
headers = state.request.trailers;
278-
break;
279-
case HeaderKind.RESPONSE_TRAILERS:
280-
headers = (state.response as BufferedResponse).buffer.trailers;
281-
}
315+
const headers = state.headers(kind);
282316

283317
const headerNames = Object.keys(headers);
284318
return this.writeNullTerminated(buf, bufLimit, headerNames);
@@ -296,29 +330,49 @@ class HttpHandler {
296330
}
297331

298332
const state = stateStorage.getStore()!;
299-
let headers: NodeJS.Dict<string | string[] | number>;
300-
switch (kind) {
301-
case HeaderKind.REQUEST:
302-
headers = state.request.headers;
303-
break;
304-
case HeaderKind.RESPONSE:
305-
headers = state.response.getHeaders();
306-
break;
307-
case HeaderKind.REQUEST_TRAILERS:
308-
headers = state.request.trailers;
309-
break;
310-
case HeaderKind.RESPONSE_TRAILERS:
311-
headers = (state.response as BufferedResponse).buffer.trailers;
312-
}
333+
const headers = state.headers(kind);
313334

314335
const n = this.mustReadString('name', name, nameLen).toLowerCase();
315-
let values: string[] = [];
316336
const value = headers[n];
337+
let values: string[] = [];
317338
if (value) {
318339
if (Array.isArray(value)) {
319340
values = value;
320341
} else {
321-
values.push(value.toString());
342+
// NodeJS array vs join behavior is dependent on header name
343+
// https://nodejs.org/api/http.html#messageheaders
344+
switch (n) {
345+
// TODO(anuraaga): date is not mentioned as a header where duplicates are discarded.
346+
// However, since it has a comma inside, it seems it must be handled as a single
347+
// string. Double-check this.
348+
case 'date':
349+
case 'age':
350+
case 'authorization':
351+
case 'content-length':
352+
case 'content-type':
353+
case 'etag':
354+
case 'expires':
355+
case 'from':
356+
case 'host':
357+
case 'if-modified-since':
358+
case 'if-unmodified-since':
359+
case 'last-modified':
360+
case 'location':
361+
case 'max-forwards':
362+
case 'proxy-authorization':
363+
case 'referer':
364+
case 'retry-after':
365+
case 'server':
366+
case 'user-agent':
367+
values = [value as string];
368+
break;
369+
case 'cookie':
370+
values = (value as string).split('; ');
371+
break;
372+
default:
373+
values = (value as string).split(', ');
374+
break;
375+
}
322376
}
323377
}
324378

@@ -391,7 +445,7 @@ class HttpHandler {
391445
const start = state.requestBodyReadIndex;
392446
const end = Math.min(start + bufLimit, body.length);
393447
const slice = body.subarray(start, end);
394-
this.memory.set(slice, buf);
448+
this.memoryBuffer.set(slice, buf);
395449
state.requestBodyReadIndex = end;
396450
if (end === body.length) {
397451
return (1n << 32n) | BigInt(slice.length);
@@ -411,7 +465,7 @@ class HttpHandler {
411465
const start = state.responseBodyReadIndex;
412466
const end = Math.min(start + bufLimit, body.length);
413467
const slice = body.subarray(start, end);
414-
this.memory.set(slice, buf);
468+
this.memoryBuffer.set(slice, buf);
415469
state.responseBodyReadIndex = end;
416470
if (end === body.length) {
417471
return (1n << 32n) | BigInt(slice.length);
@@ -457,7 +511,7 @@ class HttpHandler {
457511
}
458512
}
459513

460-
private setHeader(
514+
private addHeader(
461515
kind: HeaderKind,
462516
name: number,
463517
nameLen: number,
@@ -467,13 +521,47 @@ class HttpHandler {
467521
if (nameLen == 0) {
468522
throw new Error('HTTP header name cannot be empty');
469523
}
470-
if (kind !== HeaderKind.RESPONSE) {
471-
throw new Error('TODO: Support non-response set_header');
524+
525+
const n = this.mustReadString('name', name, nameLen);
526+
const v = this.mustReadString('value', value, valueLen);
527+
528+
const headers = stateStorage.getStore()!.headers(kind);
529+
const existing = headers[n];
530+
let newValue: string[];
531+
if (existing) {
532+
newValue = Array.isArray(existing)
533+
? existing.concat(v)
534+
: [existing.toString(), v];
535+
} else {
536+
newValue = [v];
537+
}
538+
stateStorage.getStore()!.setHeader(kind, n, newValue);
539+
}
540+
541+
private setHeader(
542+
kind: HeaderKind,
543+
name: number,
544+
nameLen: number,
545+
value: number,
546+
valueLen: number,
547+
): void {
548+
if (nameLen == 0) {
549+
throw new Error('HTTP header name cannot be empty');
472550
}
473551

474552
const n = this.mustReadString('name', name, nameLen);
475553
const v = this.mustReadString('value', value, valueLen);
476-
stateStorage.getStore()?.response.setHeader(n, v);
554+
555+
stateStorage.getStore()!.setHeader(kind, n, [v]);
556+
}
557+
558+
private removeHeader(kind: HeaderKind, name: number, nameLen: number): void {
559+
if (nameLen == 0) {
560+
throw new Error('HTTP header name cannot be empty');
561+
}
562+
563+
const n = this.mustReadString('name', name, nameLen);
564+
stateStorage.getStore()?.removeHeader(kind, n);
477565
}
478566

479567
private setStatusCode(statusCode: number): void {
@@ -510,13 +598,15 @@ class HttpHandler {
510598
}
511599

512600
if (
513-
offset >= this.memory.length ||
514-
offset + byteCount >= this.memory.length
601+
offset >= this.memoryBuffer.length ||
602+
offset + byteCount >= this.memoryBuffer.length
515603
) {
516-
throw new Error(`out of memory reading ${fieldName}`);
604+
throw new Error(
605+
`out of memory reading ${fieldName}, offset: ${offset}, byteCount: ${byteCount}`,
606+
);
517607
}
518608

519-
return this.memory.slice(offset, offset + byteCount);
609+
return this.memoryBuffer.slice(offset, offset + byteCount);
520610
}
521611

522612
private writeStringIfUnderLimit(
@@ -533,7 +623,7 @@ class HttpHandler {
533623
return vLen;
534624
}
535625

536-
this.memory.set(v, offset);
626+
this.memoryBuffer.set(v, offset);
537627
return vLen;
538628
}
539629

@@ -559,14 +649,18 @@ class HttpHandler {
559649
let offset = 0;
560650
for (const s of encodedInput) {
561651
const sLen = s.length;
562-
this.memory.set(s, buf + offset);
652+
this.memoryBuffer.set(s, buf + offset);
563653
offset += sLen;
564-
this.memory[buf + offset] = 0;
654+
this.memoryBuffer[buf + offset] = 0;
565655
offset++;
566656
}
567657

568658
return countLen;
569659
}
660+
661+
private get memoryBuffer(): Uint8Array {
662+
return new Uint8Array(this.memory.buffer);
663+
}
570664
}
571665

572666
const host = (wasi: WASI, httpHandler: HttpHandler) => {
@@ -611,18 +705,23 @@ export default async (options: Options) => {
611705
}
612706
const state = new RequestState(req, res);
613707
stateStorage.run(state, () => {
614-
const ctxNext = handleRequest();
615-
if ((ctxNext & 0x1n) !== 0x1n) {
616-
// wasm populated a response so end it.
617-
res.end();
618-
} else {
619-
next();
620-
state.nextCalled = true;
621-
const ctx = Number(ctxNext >> 32n);
622-
handleResponse(ctx);
623-
}
624-
if (mwState.features.has(Feature.BUFFER_RESPONSE)) {
625-
(res as BufferedResponse).buffer.release();
708+
try {
709+
const ctxNext = handleRequest();
710+
if ((ctxNext & 0x1n) !== 0x1n) {
711+
// wasm populated a response so end it.
712+
res.end();
713+
} else {
714+
next();
715+
state.nextCalled = true;
716+
const ctx = Number(ctxNext >> 32n);
717+
handleResponse(ctx);
718+
}
719+
if (mwState.features.has(Feature.BUFFER_RESPONSE)) {
720+
(res as BufferedResponse).buffer.release();
721+
}
722+
} catch (e) {
723+
console.log('exception in wasm middleware', e);
724+
throw e;
626725
}
627726
});
628727
});

‎packages/express/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"raw-body": "^2.5.1"
2626
},
2727
"devDependencies": {
28-
"@types/express": "^4.17.14"
28+
"@types/express": "^4.17.14",
29+
"testcontainers": "^9.1.1"
2930
}
3031
}

‎packages/express/test/example.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,9 @@ describe('wasi middleware', async function () {
394394
`
395395
POST / HTTP/1.1
396396
accept: */*
397-
accept-encoding: gzip, deflate, br
397+
accept-encoding: gzip
398+
accept-encoding: deflate
399+
accept-encoding: br
398400
connection: close
399401
content-length: 18
400402
content-type: application/json

0 commit comments

Comments
 (0)