@travetto/web 6.0.0 → 6.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import type { WebInterceptor } from '../types/interceptor.ts';
5
5
  import type { WebChainedFilter, WebFilter } from '../types/filter.ts';
6
6
  import { HttpMethod } from '../types/core.ts';
7
7
  import { WebHeaders } from '../types/headers.ts';
8
- import { WebResponse } from '../types/response.ts';
8
+ import { WebResponse, WebResponseContext } from '../types/response.ts';
9
9
  import { WebRequest } from '../types/request.ts';
10
10
 
11
11
  export type EndpointFunction = TypedFunction<Any, unknown>;
@@ -78,6 +78,10 @@ interface CoreConfig {
78
78
  * Response headers
79
79
  */
80
80
  responseHeaders?: Record<string, string>;
81
+ /**
82
+ * Partial response context
83
+ */
84
+ responseContext?: Partial<WebResponseContext>;
81
85
  }
82
86
 
83
87
  /**
@@ -167,14 +171,14 @@ export interface EndpointConfig extends CoreConfig, DescribableConfig {
167
171
  * Full path including controller
168
172
  */
169
173
  fullPath: string;
170
- /**
171
- * Response header map
172
- */
173
- responseHeaderMap: WebHeaders;
174
174
  /**
175
175
  * Response finalizer
176
176
  */
177
177
  responseFinalizer?: (res: WebResponse) => WebResponse;
178
+ /**
179
+ * Response headers finalized
180
+ */
181
+ finalizedResponseHeaders: WebHeaders;
178
182
  }
179
183
 
180
184
  /**
@@ -9,7 +9,7 @@ export type Cookie = {
9
9
  priority?: 'low' | 'medium' | 'high';
10
10
  sameSite?: 'strict' | 'lax' | 'none';
11
11
  secure?: boolean;
12
- httpOnly?: boolean;
12
+ httponly?: boolean;
13
13
  partitioned?: boolean;
14
14
  response?: boolean;
15
15
  };
@@ -1,19 +1,14 @@
1
- import { Any, ByteRange, castTo } from '@travetto/runtime';
2
- import { MimeType, MimeUtil } from '../util/mime.ts';
1
+ import { Any, castTo } from '@travetto/runtime';
3
2
 
4
3
  type Prim = number | boolean | string;
5
4
  type HeaderValue = Prim | Prim[] | readonly Prim[];
6
5
  export type WebHeadersInit = Headers | Record<string, undefined | null | HeaderValue> | [string, HeaderValue][];
7
6
 
8
- const FILENAME_EXTRACT = /filename[*]?=["]?([^";]*)["]?/;
9
-
10
7
  /**
11
8
  * Simple Headers wrapper with additional logic for common patterns
12
9
  */
13
10
  export class WebHeaders extends Headers {
14
11
 
15
- #parsedType?: MimeType;
16
-
17
12
  constructor(o?: WebHeadersInit) {
18
13
  const passed = (o instanceof Headers);
19
14
  super(passed ? o : undefined);
@@ -56,42 +51,6 @@ export class WebHeaders extends Headers {
56
51
  }
57
52
  }
58
53
 
59
- /**
60
- * Vary a value
61
- */
62
- vary(value: string): void {
63
- this.append('Vary', value);
64
- }
65
-
66
- /**
67
- * Get the fully parsed content type
68
- */
69
- getContentType(): MimeType | undefined {
70
- return this.#parsedType ??= MimeUtil.parse(this.get('Content-Type')!);
71
- }
72
-
73
- /**
74
- * Read the filename from the content disposition
75
- */
76
- getFilename(): string | undefined {
77
- const [, match] = (this.get('Content-Disposition') ?? '').match(FILENAME_EXTRACT) ?? [];
78
- return match;
79
- }
80
-
81
- /**
82
- * Get requested byte range for a given request
83
- */
84
- getRange(chunkSize: number = 100 * 1024): ByteRange | undefined {
85
- const rangeHeader = this.get('Range');
86
- if (rangeHeader) {
87
- const [start, end] = rangeHeader.replace(/bytes=/, '').split('-')
88
- .map(x => x ? parseInt(x, 10) : undefined);
89
- if (start !== undefined) {
90
- return { start, end: end ?? (start + chunkSize) };
91
- }
92
- }
93
- }
94
-
95
54
  /**
96
55
  * Set header value with a prefix
97
56
  */
@@ -2,6 +2,8 @@ import { BaseWebMessage } from './message.ts';
2
2
 
3
3
  export interface WebResponseContext {
4
4
  httpStatusCode?: number;
5
+ isPrivate?: boolean;
6
+ cacheableAge?: number;
5
7
  }
6
8
 
7
9
  /**
package/src/util/body.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { TextDecoder } from 'node:util';
2
2
  import { Readable } from 'node:stream';
3
- import { buffer as toBuffer } from 'node:stream/consumers';
4
3
 
5
4
  import { Any, BinaryUtil, castTo, hasToJSON, Util } from '@travetto/runtime';
6
5
 
@@ -15,20 +14,6 @@ const WebRawStreamSymbol = Symbol();
15
14
  */
16
15
  export class WebBodyUtil {
17
16
 
18
- /**
19
- * Convert a node binary input to a buffer
20
- */
21
- static async toBuffer(src: WebBinaryBody): Promise<Buffer> {
22
- return Buffer.isBuffer(src) ? src : toBuffer(src);
23
- }
24
-
25
- /**
26
- * Convert a node binary input to a readable
27
- */
28
- static toReadable(src: WebBinaryBody): Readable {
29
- return Buffer.isBuffer(src) ? Readable.from(src) : src;
30
- }
31
-
32
17
  /**
33
18
  * Generate multipart body
34
19
  */
@@ -1,17 +1,15 @@
1
- import { AppError, ErrorCategory, TimeSpan, TimeUtil } from '@travetto/runtime';
1
+ import { AppError, ErrorCategory, Util } from '@travetto/runtime';
2
2
 
3
3
  import { WebResponse } from '../types/response.ts';
4
4
  import { WebRequest } from '../types/request.ts';
5
+ import { WebError } from '../types/error.ts';
5
6
 
6
7
  type List<T> = T[] | readonly T[];
7
8
  type OrderedState<T> = { after?: List<T>, before?: List<T>, key: T };
8
9
 
9
10
  const WebRequestParamsSymbol = Symbol();
10
11
 
11
- export type CacheControlFlag =
12
- 'must-revalidate' | 'public' | 'private' | 'no-cache' |
13
- 'no-store' | 'no-transform' | 'proxy-revalidate' | 'immutable' |
14
- 'must-understand' | 'stale-if-error' | 'stale-while-revalidate';
12
+ export type ByteSizeInput = `${number}${'mb' | 'kb' | 'gb' | 'b' | ''}` | number;
15
13
 
16
14
  /**
17
15
  * Mapping from error category to standard http error codes
@@ -33,6 +31,12 @@ export class WebCommonUtil {
33
31
  gb: 2 ** 30,
34
32
  };
35
33
 
34
+ static #convert(rule: string): RegExp {
35
+ const core = (rule.endsWith('/*') || !rule.includes('/')) ?
36
+ `${rule.replace(/[/].{0,20}$/, '')}\/.*` : rule;
37
+ return new RegExp(`^${core}[ ]{0,10}(;|$)`);
38
+ }
39
+
36
40
  static #buildEdgeMap<T, U extends OrderedState<T>>(items: List<U>): Map<T, Set<T>> {
37
41
  const edgeMap = new Map(items.map(x => [x.key, new Set(x.after ?? [])]));
38
42
 
@@ -51,6 +55,19 @@ export class WebCommonUtil {
51
55
  return edgeMap;
52
56
  }
53
57
 
58
+
59
+ /**
60
+ * Build matcher
61
+ */
62
+ static mimeTypeMatcher(rules: string[] | string = []): (contentType: string) => boolean {
63
+ return Util.allowDeny<RegExp, [string]>(
64
+ rules,
65
+ this.#convert.bind(this),
66
+ (regex, mime) => regex.test(mime),
67
+ k => k
68
+ );
69
+ }
70
+
54
71
  /**
55
72
  * Produces a satisfied ordering for a list of orderable elements
56
73
  */
@@ -103,7 +120,7 @@ export class WebCommonUtil {
103
120
  new AppError(err.message, { details: err }) :
104
121
  new AppError(`${err}`);
105
122
 
106
- const error: Error & { category?: ErrorCategory, details?: { statusCode: number } } = body;
123
+ const error: Error & Partial<WebError> = body;
107
124
  const statusCode = error.details?.statusCode ?? ERROR_CATEGORY_STATUS[error.category!] ?? 500;
108
125
 
109
126
  return new WebResponse({ body, context: { httpStatusCode: statusCode } });
@@ -123,19 +140,13 @@ export class WebCommonUtil {
123
140
  request[WebRequestParamsSymbol] ??= params;
124
141
  }
125
142
 
126
- /**
127
- * Get a cache control value
128
- */
129
- static getCacheControlValue(value: number | TimeSpan, flags: CacheControlFlag[] = []): string {
130
- const delta = TimeUtil.asSeconds(value);
131
- const finalFlags = delta === 0 ? ['no-cache'] : flags;
132
- return [...finalFlags, `max-age=${delta}`].join(',');
133
- }
134
-
135
143
  /**
136
144
  * Parse byte size
137
145
  */
138
- static parseByteSize(input: `${number}${'mb' | 'kb' | 'gb' | 'b' | ''}`): number {
146
+ static parseByteSize(input: ByteSizeInput): number {
147
+ if (typeof input === 'number') {
148
+ return input;
149
+ }
139
150
  const [, num, unit] = input.toLowerCase().split(/(\d+)/);
140
151
  return parseInt(num, 10) * (this.#unitMapping[unit] ?? 1);
141
152
  }
@@ -1,7 +1,8 @@
1
- import keygrip from 'keygrip';
2
- import { AppError, castKey, castTo } from '@travetto/runtime';
1
+ import { AppError } from '@travetto/runtime';
3
2
 
4
3
  import { Cookie, CookieGetOptions, CookieSetOptions } from '../types/cookie.ts';
4
+ import { KeyGrip } from './keygrip.ts';
5
+ import { WebHeaderUtil } from './header.ts';
5
6
 
6
7
  const pairText = (c: Cookie): string => `${c.name}=${c.value}`;
7
8
  const pair = (k: string, v: unknown): string => `${k}=${v}`;
@@ -10,55 +11,13 @@ type CookieJarOptions = { keys?: string[] } & CookieSetOptions;
10
11
 
11
12
  export class CookieJar {
12
13
 
13
- static parseCookieHeader(header: string): Cookie[] {
14
- return header.split(/\s{0,4};\s{0,4}/g)
15
- .map(x => x.trim())
16
- .filter(x => !!x)
17
- .map(item => {
18
- const kv = item.split(/\s{0,4}=\s{0,4}/);
19
- return { name: kv[0], value: kv[1] };
20
- });
21
- }
22
-
23
- static parseSetCookieHeader(header: string): Cookie {
24
- const parts = header.split(/\s{0,4};\s{0,4}/g);
25
- const [name, value] = parts[0].split(/\s{0,4}=\s{0,4}/);
26
- const c: Cookie = { name, value };
27
- for (const p of parts.slice(1)) {
28
- // eslint-disable-next-line prefer-const
29
- let [k, v = ''] = p.split(/\s{0,4}=\s{0,4}/);
30
- if (v[0] === '"') {
31
- v = v.slice(1, -1);
32
- }
33
- if (k === 'expires') {
34
- c[k] = new Date(v);
35
- } else {
36
- c[castKey(k)] = castTo(v || true);
37
- }
38
- }
39
- return c;
40
- }
41
-
42
- static responseSuffix(c: Cookie): string[] {
43
- const parts = [];
44
- if (c.path) { parts.push(pair('path', c.path)); }
45
- if (c.expires) { parts.push(pair('expires', c.expires.toUTCString())); }
46
- if (c.domain) { parts.push(pair('domain', c.domain)); }
47
- if (c.priority) { parts.push(pair('priority', c.priority.toLowerCase())); }
48
- if (c.sameSite) { parts.push(pair('samesite', c.sameSite.toLowerCase())); }
49
- if (c.secure) { parts.push('secure'); }
50
- if (c.httpOnly) { parts.push('httponly'); }
51
- if (c.partitioned) { parts.push('partitioned'); }
52
- return parts;
53
- }
54
-
55
- #grip?: keygrip;
14
+ #grip?: KeyGrip;
56
15
  #cookies: Record<string, Cookie> = {};
57
16
  #setOptions: CookieSetOptions = {};
58
17
  #deleteOptions: CookieSetOptions = { maxAge: 0, expires: undefined };
59
18
 
60
19
  constructor({ keys, ...options }: CookieJarOptions = {}) {
61
- this.#grip = keys?.length ? new keygrip(keys) : undefined;
20
+ this.#grip = keys?.length ? new KeyGrip(keys) : undefined;
62
21
  this.#setOptions = {
63
22
  secure: false,
64
23
  path: '/',
@@ -68,7 +27,7 @@ export class CookieJar {
68
27
  }
69
28
 
70
29
  #exportCookie(cookie: Cookie, response?: boolean): string[] {
71
- const suffix = response ? CookieJar.responseSuffix(cookie) : null;
30
+ const suffix = response ? WebHeaderUtil.buildCookieSuffix(cookie) : null;
72
31
  const payload = pairText(cookie);
73
32
  const out = suffix ? [[payload, ...suffix].join(';')] : [payload];
74
33
  if (cookie.signed) {
@@ -144,11 +103,11 @@ export class CookieJar {
144
103
  }
145
104
 
146
105
  importCookieHeader(header: string | null | undefined): this {
147
- return this.import(CookieJar.parseCookieHeader(header ?? ''));
106
+ return this.import(WebHeaderUtil.parseCookieHeader(header ?? ''));
148
107
  }
149
108
 
150
109
  importSetCookieHeader(headers: string[] | null | undefined): this {
151
- return this.import(headers?.map(CookieJar.parseSetCookieHeader) ?? []);
110
+ return this.import(headers?.map(WebHeaderUtil.parseSetCookieHeader) ?? []);
152
111
  }
153
112
 
154
113
  exportCookieHeader(): string {
@@ -143,10 +143,15 @@ export class EndpointUtil {
143
143
  try {
144
144
  const params = await this.extractParameters(endpoint, request);
145
145
  const body = await endpoint.endpoint.apply(endpoint.instance, params);
146
- const headers = endpoint.responseHeaderMap;
147
- const response = body instanceof WebResponse ? body : new WebResponse({ body, headers });
148
- if (response === body) {
149
- for (const [k, v] of headers) { response.headers.setIfAbsent(k, v); }
146
+ const headers = endpoint.finalizedResponseHeaders;
147
+ let response: WebResponse;
148
+ if (body instanceof WebResponse) {
149
+ for (const [k, v] of headers) { body.headers.setIfAbsent(k, v); }
150
+ // Rewrite context
151
+ Object.assign(body.context, { ...endpoint.responseContext, ...body.context });
152
+ response = body;
153
+ } else {
154
+ response = new WebResponse({ body, headers, context: { ...endpoint.responseContext } });
150
155
  }
151
156
  return endpoint.responseFinalizer?.(response) ?? response;
152
157
  } catch (err) {
@@ -0,0 +1,161 @@
1
+ import { ByteRange, castKey, castTo } from '@travetto/runtime';
2
+
3
+ import { type Cookie } from '../types/cookie.ts';
4
+ import { WebHeaders } from '../types/headers.ts';
5
+
6
+ export type WebParsedHeader = { value: string, parameters: Record<string, string>, q?: number };
7
+
8
+ const SPLIT_EQ = /[ ]{0,10}=[ ]{0,10}/g;
9
+ const SPLIT_COMMA = /[ ]{0,10},[ ]{0,10}/g;
10
+ const SPLIT_SEMI = /[ ]{0,10};[ ]{0,10}/g;
11
+ const QUOTE = '"'.charCodeAt(0);
12
+
13
+ /**
14
+ * Web header utils
15
+ */
16
+ export class WebHeaderUtil {
17
+
18
+ /**
19
+ * Parse cookie header
20
+ */
21
+ static parseCookieHeader(header: string): Cookie[] {
22
+ const val = header.trim();
23
+ return !val ? [] : val.split(SPLIT_SEMI).map(item => {
24
+ const [name, value] = item.split(SPLIT_EQ);
25
+ return { name, value };
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Parse cookie set header
31
+ */
32
+ static parseSetCookieHeader(header: string): Cookie {
33
+ const parts = header.split(SPLIT_SEMI);
34
+ const [name, value] = parts[0].split(SPLIT_EQ);
35
+ const c: Cookie = { name, value };
36
+ for (const p of parts.slice(1)) {
37
+ const [k, pv = ''] = p.toLowerCase().split(SPLIT_EQ);
38
+ const v = pv.charCodeAt(0) === QUOTE ? pv.slice(1, -1) : pv;
39
+ if (k === 'expires') {
40
+ c[k] = new Date(v);
41
+ } else {
42
+ c[castKey(k)] = castTo(v || true);
43
+ }
44
+ }
45
+ return c;
46
+ }
47
+
48
+ /**
49
+ * Parse header segment
50
+ * @input input
51
+ */
52
+ static parseHeaderSegment(input: string | null | undefined): WebParsedHeader {
53
+ if (!input) {
54
+ return { value: '', parameters: {} };
55
+ }
56
+ const [rv, ...parts] = input.split(SPLIT_SEMI);
57
+ const item: WebParsedHeader = { value: '', parameters: {} };
58
+ const value = rv.charCodeAt(0) === QUOTE ? rv.slice(1, -1) : rv;
59
+ if (value.includes('=')) {
60
+ parts.unshift(value);
61
+ } else {
62
+ item.value = value;
63
+ }
64
+ for (const part of parts) {
65
+ const [k, pv = ''] = part.split(SPLIT_EQ);
66
+ const v = (pv.charCodeAt(0) === QUOTE) ? pv.slice(1, -1) : pv;
67
+ item.parameters[k] = v;
68
+ if (k === 'q') {
69
+ item.q = parseFloat(v);
70
+ }
71
+ }
72
+ return item;
73
+ }
74
+
75
+ /**
76
+ * Parse full header
77
+ */
78
+ static parseHeader(input: string): WebParsedHeader[] {
79
+ const v = input.trim();
80
+ if (!input) { return []; }
81
+ return v.split(SPLIT_COMMA).map(x => this.parseHeaderSegment(x));
82
+ }
83
+
84
+ /**
85
+ * Build cookie suffix
86
+ */
87
+ static buildCookieSuffix(c: Cookie): string[] {
88
+ const parts = [];
89
+ if (c.path) { parts.push(`path=${c.path}`); }
90
+ if (c.expires) { parts.push(`expires=${c.expires.toUTCString()}`); }
91
+ if (c.domain) { parts.push(`domain=${c.domain}`); }
92
+ if (c.priority) { parts.push(`priority=${c.priority.toLowerCase()}`); }
93
+ if (c.sameSite) { parts.push(`samesite=${c.sameSite.toLowerCase()}`); }
94
+ if (c.secure) { parts.push('secure'); }
95
+ if (c.httponly) { parts.push('httponly'); }
96
+ if (c.partitioned) { parts.push('partitioned'); }
97
+ return parts;
98
+ }
99
+
100
+ /**
101
+ * Negotiate header
102
+ */
103
+ static negotiateHeader<K extends string>(header: string, values: K[]): K | undefined {
104
+ if (header === '*' || header === '*/*') {
105
+ return values[0];
106
+ }
107
+ const sorted = this.parseHeader(header.toLowerCase()).filter(x => (x.q ?? 1) > 0).toSorted((a, b) => (b.q ?? 1) - (a.q ?? 1));
108
+ const set = new Set(values);
109
+ for (const { value } of sorted) {
110
+ const vk: K = castKey(value);
111
+ if (value === '*') {
112
+ return values[0];
113
+ } else if (set.has(vk)) {
114
+ return vk;
115
+ }
116
+ }
117
+ return undefined;
118
+ }
119
+
120
+ /**
121
+ * Get requested byte range for a given request
122
+ */
123
+ static getRange(headers: WebHeaders, chunkSize: number = 100 * 1024): ByteRange | undefined {
124
+ if (!headers.has('Range')) {
125
+ return;
126
+ }
127
+ const { parameters } = this.parseHeaderSegment(headers.get('Range'));
128
+ if ('bytes' in parameters) {
129
+ const [start, end] = parameters.bytes.split('-')
130
+ .map(x => x ? parseInt(x, 10) : undefined);
131
+ if (start !== undefined) {
132
+ return { start, end: end ?? (start + chunkSize) };
133
+ }
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Check freshness of the response using request and response headers.
139
+ */
140
+ static isFresh(req: WebHeaders, res: WebHeaders): boolean {
141
+ const cacheControl = req.get('Cache-Control');
142
+ if (cacheControl?.includes('no-cache')) {
143
+ return false;
144
+ }
145
+
146
+ const noneMatch = req.get('If-None-Match');
147
+ if (noneMatch) {
148
+ const etag = res.get('ETag');
149
+ const validTag = (v: string): boolean => v === etag || v === `W/${etag}` || `W/${v}` === etag;
150
+ return noneMatch === '*' || (!!etag && noneMatch.split(SPLIT_COMMA).some(validTag));
151
+ } else {
152
+ const modifiedSince = req.get('If-Modified-Since');
153
+ const lastModified = res.get('Last-Modified');
154
+ if (!modifiedSince || !lastModified) {
155
+ return false;
156
+ }
157
+ const [a, b] = [Date.parse(lastModified), Date.parse(modifiedSince)];
158
+ return !(Number.isNaN(a) || Number.isNaN(b)) && a >= b;
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,43 @@
1
+ import crypto, { BinaryToTextEncoding } from 'node:crypto';
2
+ import { AppError, castKey } from '@travetto/runtime';
3
+
4
+ const CHAR_MAPPING = { '/': '_', '+': '-', '=': '' };
5
+
6
+ function timeSafeCompare(a: string, b: string): boolean {
7
+ const key = crypto.randomBytes(32);
8
+ const ah = crypto.createHmac('sha256', key).update(a).digest();
9
+ const bh = crypto.createHmac('sha256', key).update(b).digest();
10
+ return ah.length === bh.length && crypto.timingSafeEqual(ah, bh);
11
+ }
12
+
13
+ export class KeyGrip {
14
+
15
+ #keys: string[];
16
+ #algorithm: string;
17
+ #encoding: BinaryToTextEncoding;
18
+
19
+ constructor(keys: string[], algorithm = 'sha1', encoding: BinaryToTextEncoding = 'base64') {
20
+ if (!keys.length) {
21
+ throw new AppError('Keys must be defined');
22
+ }
23
+ this.#keys = keys;
24
+ this.#algorithm = algorithm;
25
+ this.#encoding = encoding;
26
+ }
27
+
28
+ sign(data: string, key?: string): string {
29
+ return crypto
30
+ .createHmac(this.#algorithm, key ?? this.#keys[0])
31
+ .update(data)
32
+ .digest(this.#encoding)
33
+ .replace(/[/+=]/g, x => CHAR_MAPPING[castKey(x)]);
34
+ }
35
+
36
+ verify(data: string, digest: string): boolean {
37
+ return this.index(data, digest) > -1;
38
+ }
39
+
40
+ index(data: string, digest: string): number {
41
+ return this.#keys.findIndex(key => timeSafeCompare(digest, this.sign(data, key)));
42
+ }
43
+ }
@@ -1,4 +1,5 @@
1
- import { buffer as toBuffer } from 'node:stream/consumers';
1
+ import { buffer } from 'node:stream/consumers';
2
+ import { Readable } from 'node:stream';
2
3
 
3
4
  import { AppError, BinaryUtil, castTo } from '@travetto/runtime';
4
5
  import { BindUtil } from '@travetto/schema';
@@ -9,6 +10,8 @@ import { DecompressInterceptor } from '../../src/interceptor/decompress.ts';
9
10
  import { WebBodyUtil } from '../../src/util/body.ts';
10
11
  import { WebCommonUtil } from '../../src/util/common.ts';
11
12
 
13
+ const toBuffer = (src: Buffer | Readable) => Buffer.isBuffer(src) ? src : buffer(src);
14
+
12
15
  /**
13
16
  * Utilities for supporting custom test dispatchers
14
17
  */
@@ -18,7 +21,7 @@ export class WebTestDispatchUtil {
18
21
  if (request.body !== undefined) {
19
22
  const sample = WebBodyUtil.toBinaryMessage(request);
20
23
  sample.headers.forEach((v, k) => request.headers.set(k, Array.isArray(v) ? v.join(',') : v));
21
- request.body = WebBodyUtil.markRaw(await WebBodyUtil.toBuffer(sample.body!));
24
+ request.body = WebBodyUtil.markRaw(await toBuffer(sample.body!));
22
25
  }
23
26
  Object.assign(request.context, { httpQuery: BindUtil.flattenPaths(request.context.httpQuery ?? {}) });
24
27
  return request;
@@ -31,7 +34,7 @@ export class WebTestDispatchUtil {
31
34
 
32
35
  if (decompress) {
33
36
  if (Buffer.isBuffer(result) || BinaryUtil.isReadable(result)) {
34
- const bufferResult = result = await WebBodyUtil.toBuffer(result);
37
+ const bufferResult = result = await toBuffer(result);
35
38
  if (bufferResult.length) {
36
39
  try {
37
40
  result = await DecompressInterceptor.decompress(
@@ -74,7 +77,7 @@ export class WebTestDispatchUtil {
74
77
 
75
78
  const body: RequestInit['body'] =
76
79
  WebBodyUtil.isRaw(request.body) ?
77
- Buffer.isBuffer(request.body) ? request.body : await toBuffer(request.body) :
80
+ await toBuffer(request.body) :
78
81
  castTo(request.body);
79
82
 
80
83
  return { path: finalPath, init: { headers, method, body } };
@@ -20,6 +20,9 @@ export class WebTestConfig implements ConfigSource {
20
20
  http: {
21
21
  ssl: { active: false },
22
22
  port: -1,
23
+ },
24
+ etag: {
25
+ minimumSize: 1
23
26
  }
24
27
  }
25
28
  },
@@ -1,47 +0,0 @@
1
- import { Injectable, Inject } from '@travetto/di';
2
- import { Config } from '@travetto/config';
3
-
4
- import { WebChainedContext } from '../types/filter.ts';
5
- import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
6
- import { WebInterceptorCategory } from '../types/core.ts';
7
- import { WebResponse } from '../types/response.ts';
8
-
9
- import { WebCommonUtil } from '../util/common.ts';
10
-
11
- import { EtagInterceptor } from './etag.ts';
12
-
13
- @Config('web.cache')
14
- export class ResponseCacheConfig {
15
- /**
16
- * Generate response cache headers
17
- */
18
- applies = true;
19
-
20
- /**
21
- * Determines how we cache
22
- */
23
- mode: 'allow' | 'deny' = 'deny';
24
- }
25
-
26
- /**
27
- * Determines if we should cache all get requests
28
- */
29
- @Injectable()
30
- export class ResponseCacheInterceptor implements WebInterceptor {
31
-
32
- category: WebInterceptorCategory = 'response';
33
- dependsOn = [EtagInterceptor];
34
-
35
- @Inject()
36
- config: ResponseCacheConfig;
37
-
38
- applies({ config, endpoint }: WebInterceptorContext<ResponseCacheConfig>): boolean {
39
- return !!endpoint.cacheable && config.applies && config.mode === 'deny';
40
- }
41
-
42
- async filter({ next }: WebChainedContext<ResponseCacheConfig>): Promise<WebResponse> {
43
- const response = await next();
44
- response.headers.setIfAbsent('Cache-Control', WebCommonUtil.getCacheControlValue(0));
45
- return response;
46
- }
47
- }