@travetto/web 6.0.0-rc.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.
Files changed (50) hide show
  1. package/README.md +734 -0
  2. package/__index__.ts +44 -0
  3. package/package.json +66 -0
  4. package/src/common/global.ts +30 -0
  5. package/src/config.ts +18 -0
  6. package/src/context.ts +49 -0
  7. package/src/decorator/common.ts +87 -0
  8. package/src/decorator/controller.ts +13 -0
  9. package/src/decorator/endpoint.ts +102 -0
  10. package/src/decorator/param.ts +64 -0
  11. package/src/interceptor/accept.ts +70 -0
  12. package/src/interceptor/body-parse.ts +123 -0
  13. package/src/interceptor/compress.ts +119 -0
  14. package/src/interceptor/context.ts +23 -0
  15. package/src/interceptor/cookies.ts +97 -0
  16. package/src/interceptor/cors.ts +94 -0
  17. package/src/interceptor/decompress.ts +91 -0
  18. package/src/interceptor/etag.ts +99 -0
  19. package/src/interceptor/logging.ts +71 -0
  20. package/src/interceptor/respond.ts +26 -0
  21. package/src/interceptor/response-cache.ts +47 -0
  22. package/src/interceptor/trust-proxy.ts +53 -0
  23. package/src/registry/controller.ts +288 -0
  24. package/src/registry/types.ts +229 -0
  25. package/src/registry/visitor.ts +52 -0
  26. package/src/router/base.ts +67 -0
  27. package/src/router/standard.ts +59 -0
  28. package/src/types/cookie.ts +18 -0
  29. package/src/types/core.ts +33 -0
  30. package/src/types/dispatch.ts +23 -0
  31. package/src/types/error.ts +10 -0
  32. package/src/types/filter.ts +7 -0
  33. package/src/types/headers.ts +108 -0
  34. package/src/types/interceptor.ts +54 -0
  35. package/src/types/message.ts +33 -0
  36. package/src/types/request.ts +22 -0
  37. package/src/types/response.ts +20 -0
  38. package/src/util/body.ts +220 -0
  39. package/src/util/common.ts +142 -0
  40. package/src/util/cookie.ts +145 -0
  41. package/src/util/endpoint.ts +277 -0
  42. package/src/util/mime.ts +36 -0
  43. package/src/util/net.ts +61 -0
  44. package/support/test/dispatch-util.ts +90 -0
  45. package/support/test/dispatcher.ts +15 -0
  46. package/support/test/suite/base.ts +61 -0
  47. package/support/test/suite/controller.ts +103 -0
  48. package/support/test/suite/schema.ts +275 -0
  49. package/support/test/suite/standard.ts +178 -0
  50. package/support/transformer.web.ts +207 -0
@@ -0,0 +1,119 @@
1
+ import { buffer } from 'node:stream/consumers';
2
+ import { BrotliOptions, constants, createBrotliCompress, createDeflate, createGzip, ZlibOptions } from 'node:zlib';
3
+
4
+ // eslint-disable-next-line @typescript-eslint/naming-convention
5
+ import Negotiator from 'negotiator';
6
+
7
+ import { Injectable, Inject } from '@travetto/di';
8
+ import { Config } from '@travetto/config';
9
+ import { castTo } from '@travetto/runtime';
10
+
11
+ import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
12
+ import { WebInterceptorCategory } from '../types/core.ts';
13
+ import { WebChainedContext } from '../types/filter.ts';
14
+ import { WebResponse } from '../types/response.ts';
15
+ import { WebBodyUtil } from '../util/body.ts';
16
+ import { WebError } from '../types/error.ts';
17
+
18
+ const COMPRESSORS = {
19
+ gzip: createGzip,
20
+ deflate: createDeflate,
21
+ br: createBrotliCompress,
22
+ };
23
+
24
+ type WebCompressEncoding = keyof typeof COMPRESSORS | 'identity';
25
+
26
+ @Config('web.compress')
27
+ export class CompressConfig {
28
+ /**
29
+ * Attempting to compressing responses
30
+ */
31
+ applies: boolean = true;
32
+ /**
33
+ * Raw encoding options
34
+ */
35
+ raw?: (ZlibOptions & BrotliOptions) | undefined;
36
+ /**
37
+ * Preferred encodings
38
+ */
39
+ preferredEncodings?: WebCompressEncoding[] = ['br', 'gzip', 'identity'];
40
+ /**
41
+ * Supported encodings
42
+ */
43
+ supportedEncodings: WebCompressEncoding[] = ['br', 'gzip', 'identity', 'deflate'];
44
+ }
45
+
46
+ /**
47
+ * Enables compression support
48
+ */
49
+ @Injectable()
50
+ export class CompressInterceptor implements WebInterceptor {
51
+
52
+ category: WebInterceptorCategory = 'response';
53
+
54
+ @Inject()
55
+ config: CompressConfig;
56
+
57
+ async compress(ctx: WebChainedContext, response: WebResponse): Promise<WebResponse> {
58
+ const { raw = {}, preferredEncodings = [], supportedEncodings } = this.config;
59
+ const { request } = ctx;
60
+
61
+ response.headers.vary('Accept-Encoding');
62
+
63
+ if (
64
+ !response.body ||
65
+ response.headers.has('Content-Encoding') ||
66
+ response.headers.get('Cache-Control')?.includes('no-transform')
67
+ ) {
68
+ return response;
69
+ }
70
+
71
+ const accepts = request.headers.get('Accept-Encoding');
72
+ const type: WebCompressEncoding | undefined =
73
+ castTo(new Negotiator({ headers: { 'accept-encoding': accepts ?? '*' } })
74
+ .encoding([...supportedEncodings, ...preferredEncodings]));
75
+
76
+ if (accepts && (!type || !accepts.includes(type))) {
77
+ throw WebError.for(`Please accept one of: ${supportedEncodings.join(', ')}. ${accepts} is not supported`, 406);
78
+ }
79
+
80
+ if (type === 'identity' || !type) {
81
+ return response;
82
+ }
83
+
84
+ const binaryResponse = new WebResponse({ context: response.context, ...WebBodyUtil.toBinaryMessage(response) });
85
+ const chunkSize = raw.chunkSize ?? constants.Z_DEFAULT_CHUNK;
86
+ const len = Buffer.isBuffer(binaryResponse.body) ? binaryResponse.body.byteLength : undefined;
87
+
88
+ if (len !== undefined && len >= 0 && len < chunkSize || !binaryResponse.body) {
89
+ return binaryResponse;
90
+ }
91
+
92
+ const opts = type === 'br' ? { params: { [constants.BROTLI_PARAM_QUALITY]: 4, ...raw.params }, ...raw } : { ...raw };
93
+ const stream = COMPRESSORS[type](opts);
94
+
95
+ // If we are compressing
96
+ binaryResponse.headers.set('Content-Encoding', type);
97
+
98
+ if (Buffer.isBuffer(binaryResponse.body)) {
99
+ stream.end(binaryResponse.body);
100
+ const out = await buffer(stream);
101
+ binaryResponse.body = out;
102
+ binaryResponse.headers.set('Content-Length', `${out.byteLength}`);
103
+ } else {
104
+ binaryResponse.body.pipe(stream);
105
+ binaryResponse.body = stream;
106
+ binaryResponse.headers.delete('Content-Length');
107
+ }
108
+
109
+ return binaryResponse;
110
+ }
111
+
112
+ applies({ config }: WebInterceptorContext<CompressConfig>): boolean {
113
+ return config.applies;
114
+ }
115
+
116
+ async filter(ctx: WebChainedContext): Promise<WebResponse> {
117
+ return this.compress(ctx, await ctx.next());
118
+ }
119
+ }
@@ -0,0 +1,23 @@
1
+ import { Injectable, Inject } from '@travetto/di';
2
+
3
+ import { WebChainedContext } from '../types/filter.ts';
4
+ import { WebResponse } from '../types/response.ts';
5
+ import { WebInterceptor } from '../types/interceptor.ts';
6
+ import { WebInterceptorCategory } from '../types/core.ts';
7
+ import { WebAsyncContext } from '../context.ts';
8
+
9
+ /**
10
+ * Enables access to contextual data when running in a web application
11
+ */
12
+ @Injectable()
13
+ export class AsyncContextInterceptor implements WebInterceptor {
14
+
15
+ category: WebInterceptorCategory = 'global';
16
+
17
+ @Inject()
18
+ context: WebAsyncContext;
19
+
20
+ filter({ request, next }: WebChainedContext): Promise<WebResponse> {
21
+ return this.context.withContext(request, next);
22
+ }
23
+ }
@@ -0,0 +1,97 @@
1
+ import { Injectable, Inject } from '@travetto/di';
2
+ import { Config } from '@travetto/config';
3
+ import { Secret } from '@travetto/schema';
4
+ import { AsyncContext, AsyncContextValue } from '@travetto/context';
5
+
6
+ import { WebChainedContext } from '../types/filter.ts';
7
+ import { WebResponse } from '../types/response.ts';
8
+ import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
9
+ import { WebInterceptorCategory } from '../types/core.ts';
10
+
11
+ import { WebConfig } from '../config.ts';
12
+ import { Cookie, CookieSetOptions } from '../types/cookie.ts';
13
+ import { CookieJar } from '../util/cookie.ts';
14
+ import { WebAsyncContext } from '../context.ts';
15
+
16
+ /**
17
+ * Web cookie configuration
18
+ */
19
+ @Config('web.cookie')
20
+ export class CookieConfig implements CookieSetOptions {
21
+ /**
22
+ * Support reading/sending cookies
23
+ */
24
+ applies = true;
25
+ /**
26
+ * Are they signed
27
+ */
28
+ signed = true;
29
+ /**
30
+ * Supported only via http (not in JS)
31
+ */
32
+ httpOnly = true;
33
+ /**
34
+ * Enforce same site policy
35
+ */
36
+ sameSite: Cookie['sameSite'] = 'lax';
37
+ /**
38
+ * The signing keys
39
+ */
40
+ @Secret()
41
+ keys?: string[];
42
+ /**
43
+ * Is the cookie only valid for https
44
+ */
45
+ secure?: boolean = false;
46
+ /**
47
+ * The domain of the cookie
48
+ */
49
+ domain?: string;
50
+ }
51
+
52
+ /**
53
+ * Loads cookies from the request, verifies, exposes, and then signs and sets
54
+ */
55
+ @Injectable()
56
+ export class CookiesInterceptor implements WebInterceptor<CookieConfig> {
57
+
58
+ #cookieJar = new AsyncContextValue<CookieJar>(this);
59
+
60
+ category: WebInterceptorCategory = 'request';
61
+
62
+ @Inject()
63
+ config: CookieConfig;
64
+
65
+ @Inject()
66
+ webConfig: WebConfig;
67
+
68
+ @Inject()
69
+ webAsyncContext: WebAsyncContext;
70
+
71
+ @Inject()
72
+ context: AsyncContext;
73
+
74
+ postConstruct(): void {
75
+ this.webAsyncContext.registerSource(CookieJar, () => this.#cookieJar.get());
76
+ }
77
+
78
+ finalizeConfig({ config }: WebInterceptorContext<CookieConfig>): CookieConfig {
79
+ const url = new URL(this.webConfig.baseUrl ?? 'x://localhost');
80
+ config.secure ??= url.protocol === 'https';
81
+ config.domain ??= url.hostname;
82
+ return config;
83
+ }
84
+
85
+ applies({ config }: WebInterceptorContext<CookieConfig>): boolean {
86
+ return config.applies;
87
+ }
88
+
89
+ async filter({ request, config, next }: WebChainedContext<CookieConfig>): Promise<WebResponse> {
90
+ const jar = new CookieJar(request.headers.get('Cookie'), config);
91
+ this.#cookieJar.set(jar);
92
+
93
+ const response = await next();
94
+ for (const c of jar.export()) { response.headers.append('Set-Cookie', c); }
95
+ return response;
96
+ }
97
+ }
@@ -0,0 +1,94 @@
1
+ import { Config } from '@travetto/config';
2
+ import { Injectable, Inject } from '@travetto/di';
3
+ import { Ignore } from '@travetto/schema';
4
+
5
+ import { WebChainedContext } from '../types/filter.ts';
6
+ import { HTTP_METHODS, HttpMethod, WebInterceptorCategory } from '../types/core.ts';
7
+ import { WebResponse } from '../types/response.ts';
8
+ import { WebRequest } from '../types/request.ts';
9
+ import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
10
+ import { WebCommonUtil } from '../util/common.ts';
11
+
12
+ /**
13
+ * Web cors support
14
+ */
15
+ @Config('web.cors')
16
+ export class CorsConfig {
17
+ /**
18
+ * Send CORS headers on responses
19
+ */
20
+ applies = true;
21
+ /**
22
+ * Allowed origins
23
+ */
24
+ origins?: string[];
25
+ /**
26
+ * Allowed http methods
27
+ */
28
+ methods?: HttpMethod[];
29
+ /**
30
+ * Allowed http headers
31
+ */
32
+ headers?: string[];
33
+ /**
34
+ * Support credentials?
35
+ */
36
+ credentials?: boolean;
37
+
38
+ @Ignore()
39
+ resolved: {
40
+ origins: Set<string>;
41
+ methods: string;
42
+ headers: string;
43
+ credentials: boolean;
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Interceptor that will provide cors support across all requests
49
+ */
50
+ @Injectable()
51
+ export class CorsInterceptor implements WebInterceptor<CorsConfig> {
52
+
53
+ category: WebInterceptorCategory = 'response';
54
+
55
+ @Inject()
56
+ config: CorsConfig;
57
+
58
+ finalizeConfig({ config }: WebInterceptorContext<CorsConfig>): CorsConfig {
59
+ config.resolved = {
60
+ origins: new Set(config.origins ?? []),
61
+ methods: (config.methods ?? Object.keys(HTTP_METHODS)).join(',').toUpperCase(),
62
+ headers: (config.headers ?? []).join(','),
63
+ credentials: !!config.credentials,
64
+ };
65
+ return config;
66
+ }
67
+
68
+ applies({ config }: WebInterceptorContext<CorsConfig>): boolean {
69
+ return config.applies;
70
+ }
71
+
72
+ decorate(request: WebRequest, resolved: CorsConfig['resolved'], response: WebResponse,): WebResponse {
73
+ const origin = request.headers.get('Origin');
74
+ if (resolved.origins.size === 0 || resolved.origins.has(origin!)) {
75
+ for (const [k, v] of [
76
+ ['Access-Control-Allow-Origin', origin || '*'],
77
+ ['Access-Control-Allow-Credentials', `${resolved.credentials}`],
78
+ ['Access-Control-Allow-Methods', resolved.methods],
79
+ ['Access-Control-Allow-Headers', resolved.headers || request.headers.get('Access-Control-Request-Headers') || '*'],
80
+ ]) {
81
+ response.headers.setIfAbsent(k, v);
82
+ }
83
+ }
84
+ return response;
85
+ }
86
+
87
+ async filter({ request, config: { resolved }, next }: WebChainedContext<CorsConfig>): Promise<WebResponse> {
88
+ try {
89
+ return this.decorate(request, resolved, await next());
90
+ } catch (err) {
91
+ throw this.decorate(request, resolved, WebCommonUtil.catchResponse(err));
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,91 @@
1
+ import { Readable } from 'node:stream';
2
+ import zlib from 'node:zlib';
3
+ import util from 'node:util';
4
+
5
+ import { Injectable, Inject } from '@travetto/di';
6
+ import { Config } from '@travetto/config';
7
+ import { castTo } from '@travetto/runtime';
8
+
9
+ import { WebChainedContext } from '../types/filter.ts';
10
+ import { WebResponse } from '../types/response.ts';
11
+ import { WebInterceptorCategory } from '../types/core.ts';
12
+ import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
13
+ import { WebHeaders } from '../types/headers.ts';
14
+
15
+ import { WebBodyUtil } from '../util/body.ts';
16
+ import { WebError } from '../types/error.ts';
17
+
18
+ const STREAM_DECOMPRESSORS = {
19
+ gzip: zlib.createGunzip,
20
+ deflate: zlib.createInflate,
21
+ br: zlib.createBrotliDecompress,
22
+ identity: (): Readable => null!
23
+ };
24
+
25
+ const BUFFER_DECOMPRESSORS = {
26
+ gzip: util.promisify(zlib.gunzip),
27
+ deflate: util.promisify(zlib.inflate),
28
+ br: util.promisify(zlib.brotliDecompress),
29
+ identity: (): Readable => null!
30
+ };
31
+
32
+ type WebDecompressEncoding = keyof typeof BUFFER_DECOMPRESSORS;
33
+
34
+ /**
35
+ * Web body parse configuration
36
+ */
37
+ @Config('web.decompress')
38
+ export class DecompressConfig {
39
+ /**
40
+ * Parse request body
41
+ */
42
+ applies: boolean = true;
43
+ /**
44
+ * Supported encodings
45
+ */
46
+ supportedEncodings: WebDecompressEncoding[] = ['br', 'gzip', 'deflate', 'identity'];
47
+ }
48
+
49
+ /**
50
+ * Decompress body
51
+ */
52
+ @Injectable()
53
+ export class DecompressInterceptor implements WebInterceptor<DecompressConfig> {
54
+
55
+
56
+ static async decompress(headers: WebHeaders, input: Buffer | Readable, config: DecompressConfig): Promise<typeof input> {
57
+ const encoding: WebDecompressEncoding | 'identity' = castTo(headers.getList('Content-Encoding')?.[0]) ?? 'identity';
58
+
59
+ if (!config.supportedEncodings.includes(encoding)) {
60
+ throw WebError.for(`Unsupported Content-Encoding: ${encoding}`, 415);
61
+ }
62
+
63
+ if (encoding === 'identity') {
64
+ return input;
65
+ }
66
+
67
+ if (Buffer.isBuffer(input)) {
68
+ return BUFFER_DECOMPRESSORS[encoding](input);
69
+ } else {
70
+ return input.pipe(STREAM_DECOMPRESSORS[encoding]());
71
+ }
72
+ }
73
+
74
+ dependsOn = [];
75
+ category: WebInterceptorCategory = 'request';
76
+
77
+ @Inject()
78
+ config: DecompressConfig;
79
+
80
+ applies({ config }: WebInterceptorContext<DecompressConfig>): boolean {
81
+ return config.applies;
82
+ }
83
+
84
+ async filter({ request, config, next }: WebChainedContext<DecompressConfig>): Promise<WebResponse> {
85
+ if (WebBodyUtil.isRaw(request.body)) {
86
+ const updatedBody = await DecompressInterceptor.decompress(request.headers, request.body, config);
87
+ request.body = WebBodyUtil.markRaw(updatedBody);
88
+ }
89
+ return next();
90
+ }
91
+ }
@@ -0,0 +1,99 @@
1
+ import crypto from 'node:crypto';
2
+ import fresh from 'fresh';
3
+
4
+ import { Injectable, Inject } from '@travetto/di';
5
+ import { Config } from '@travetto/config';
6
+ import { Ignore } from '@travetto/schema';
7
+
8
+ import { WebChainedContext } from '../types/filter.ts';
9
+ import { WebResponse } from '../types/response.ts';
10
+ import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
11
+ import { WebInterceptorCategory } from '../types/core.ts';
12
+ import { CompressInterceptor } from './compress.ts';
13
+ import { WebBodyUtil } from '../util/body.ts';
14
+
15
+ @Config('web.etag')
16
+ export class EtagConfig {
17
+ /**
18
+ * Attempt ETag generation
19
+ */
20
+ applies = true;
21
+ /**
22
+ * Should we generate a weak etag
23
+ */
24
+ weak?: boolean;
25
+
26
+ @Ignore()
27
+ cacheable?: boolean;
28
+ }
29
+
30
+ /**
31
+ * Enables etag support
32
+ */
33
+ @Injectable()
34
+ export class EtagInterceptor implements WebInterceptor {
35
+
36
+ category: WebInterceptorCategory = 'response';
37
+ dependsOn = [CompressInterceptor];
38
+
39
+ @Inject()
40
+ config: EtagConfig;
41
+
42
+ computeTag(body: Buffer): string {
43
+ return body.byteLength === 0 ?
44
+ '2jmj7l5rSw0yVb/vlWAYkK/YBwk' :
45
+ crypto
46
+ .createHash('sha1')
47
+ .update(body.toString('utf8'), 'utf8')
48
+ .digest('base64')
49
+ .substring(0, 27);
50
+ }
51
+
52
+ addTag(ctx: WebChainedContext<EtagConfig>, response: WebResponse): WebResponse {
53
+ const { request } = ctx;
54
+
55
+ const statusCode = response.context.httpStatusCode;
56
+
57
+ if (statusCode && (statusCode >= 300 && statusCode !== 304)) {
58
+ return response;
59
+ }
60
+
61
+ const binaryResponse = new WebResponse({ ...response, ...WebBodyUtil.toBinaryMessage(response) });
62
+ if (!Buffer.isBuffer(binaryResponse.body)) {
63
+ return binaryResponse;
64
+ }
65
+
66
+ const tag = this.computeTag(binaryResponse.body);
67
+ binaryResponse.headers.set('ETag', `${ctx.config.weak ? 'W/' : ''}"${tag}"`);
68
+
69
+ if (ctx.config.cacheable &&
70
+ fresh({
71
+ 'if-modified-since': request.headers.get('If-Modified-Since')!,
72
+ 'if-none-match': request.headers.get('If-None-Match')!,
73
+ 'cache-control': request.headers.get('Cache-Control')!,
74
+ }, {
75
+ etag: binaryResponse.headers.get('ETag')!,
76
+ 'last-modified': binaryResponse.headers.get('Last-Modified')!
77
+ })
78
+ ) {
79
+ return new WebResponse({ context: { httpStatusCode: 304 } });
80
+ }
81
+
82
+ return binaryResponse;
83
+ }
84
+
85
+ finalizeConfig({ config, endpoint }: WebInterceptorContext<EtagConfig>): EtagConfig {
86
+ if (endpoint.cacheable) {
87
+ return { ...config, cacheable: true };
88
+ }
89
+ return config;
90
+ }
91
+
92
+ applies({ config }: WebInterceptorContext<EtagConfig>): boolean {
93
+ return config.applies;
94
+ }
95
+
96
+ async filter(ctx: WebChainedContext<EtagConfig>): Promise<WebResponse> {
97
+ return this.addTag(ctx, await ctx.next());
98
+ }
99
+ }
@@ -0,0 +1,71 @@
1
+ import { Injectable, Inject } from '@travetto/di';
2
+ import { Config } from '@travetto/config';
3
+
4
+ import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
5
+ import { WebInterceptorCategory } from '../types/core.ts';
6
+ import { WebChainedContext } from '../types/filter.ts';
7
+ import { WebResponse } from '../types/response.ts';
8
+
9
+ /**
10
+ * Web logging configuration
11
+ */
12
+ @Config('web.log')
13
+ export class WebLogConfig {
14
+ /**
15
+ * Enable logging of all requests
16
+ */
17
+ applies = true;
18
+ /**
19
+ * Should errors be dumped as full stack traces
20
+ */
21
+ showStackTrace = true;
22
+ }
23
+
24
+ /**
25
+ * Logging interceptor, to show activity for all requests
26
+ */
27
+ @Injectable()
28
+ export class LoggingInterceptor implements WebInterceptor {
29
+
30
+ category: WebInterceptorCategory = 'terminal';
31
+
32
+ @Inject()
33
+ config: WebLogConfig;
34
+
35
+ applies({ config }: WebInterceptorContext<WebLogConfig>): boolean {
36
+ return config.applies;
37
+ }
38
+
39
+ async filter({ request, next }: WebChainedContext): Promise<WebResponse> {
40
+ const createdDate = Date.now();
41
+ const response = await next();
42
+ const duration = Date.now() - createdDate;
43
+
44
+ const err = response.body instanceof Error ? response.body : undefined;
45
+ const code = response.context.httpStatusCode ??= (!!err ? 500 : 200);
46
+
47
+ const logMessage = {
48
+ method: request.context.httpMethod,
49
+ path: request.context.path,
50
+ query: request.context.httpQuery,
51
+ params: request.context.pathParams,
52
+ statusCode: code,
53
+ duration,
54
+ };
55
+
56
+ if (code < 400) {
57
+ console.info('Request', logMessage);
58
+ } else if (code < 500) {
59
+ console.warn('Request', logMessage);
60
+ } else {
61
+ console.error('Request', logMessage);
62
+ }
63
+
64
+ if (this.config.showStackTrace && err) {
65
+ console.error(err.message, { error: err });
66
+ }
67
+
68
+ return response;
69
+ }
70
+ }
71
+
@@ -0,0 +1,26 @@
1
+ import { Injectable } from '@travetto/di';
2
+
3
+ import { WebInterceptor } from '../types/interceptor.ts';
4
+ import { WebInterceptorCategory } from '../types/core.ts';
5
+ import { WebResponse } from '../types/response.ts';
6
+
7
+ import { WebChainedContext } from '../types/filter.ts';
8
+ import { LoggingInterceptor } from './logging.ts';
9
+ import { WebCommonUtil } from '../util/common.ts';
10
+
11
+ @Injectable()
12
+ export class RespondInterceptor implements WebInterceptor {
13
+
14
+ category: WebInterceptorCategory = 'terminal';
15
+ dependsOn = [LoggingInterceptor];
16
+
17
+ async filter(ctx: WebChainedContext): Promise<WebResponse> {
18
+ let res;
19
+ try {
20
+ res = await ctx.next();
21
+ } catch (err) {
22
+ res = WebCommonUtil.catchResponse(err);
23
+ }
24
+ return res;
25
+ }
26
+ }
@@ -0,0 +1,47 @@
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
+ }