-
Notifications
You must be signed in to change notification settings - Fork 652
Expand file tree
/
Copy pathaposResponse.js
More file actions
146 lines (123 loc) · 4.9 KB
/
Copy pathaposResponse.js
File metadata and controls
146 lines (123 loc) · 4.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import config from 'virtual:apostrophe-config';
import { request } from 'undici';
import zlib from 'zlib';
import { promisify } from 'util';
// Promisify zlib functions
const gunzipAsync = promisify(zlib.gunzip);
const inflateAsync = promisify(zlib.inflate);
const brotliDecompressAsync = promisify(zlib.brotliDecompress);
const excludedHeadersLower = new Set(config.excludeRequestHeaders?.map(h => h.toLowerCase()) || []);
function looksLikeChunkedEncoding(buffer) {
const str = buffer.toString('utf8');
// chunked responses start with a hex line followed by \r\n
// and end with "0\r\n\r\n"
const startsWithHex = /^[0-9a-fA-F]+\r\n/.test(str);
const endsWithTerminator = /0\r\n\r\n$/.test(str);
return startsWithHex && endsWithTerminator;
}
export default async function aposResponse(req) {
try {
// Host header should not contain the protocol or a path
const host = req.headers.get('host');
if (host?.includes('/')) {
return new Response('Invalid Host header', {
status: 400,
statusText: 'Bad Request'
});
}
// Prepare URL for the backend request
const url = new URL(req.url);
const aposHost = config.aposHost;
let pathname = url.pathname;
// Apostrophe redirects prefix-only paths (e.g. `/my-prefix`) to
// the trailing-slash form (`/my-prefix/`) with a 301. During
// static builds Astro may request the base path without the slash,
// causing aposPageFetch to receive HTML instead of JSON.
// Ensure a trailing slash when the pathname is exactly the prefix.
if (config.aposPrefix && pathname === config.aposPrefix) {
pathname += '/';
}
const aposUrl = new URL(aposHost + pathname);
aposUrl.search = url.search;
// Prepare headers, excluding any specified in config
const requestHeaders = {};
for (const [name, value] of req.headers) {
if (!excludedHeadersLower.has(name.toLowerCase())) {
requestHeaders[name] = value;
}
}
// Make the request to the backend
const res = await request(aposUrl.href, {
headers: requestHeaders,
method: req.method,
body: req.body
});
// Prepare response headers
const responseHeaders = new Headers();
Object.entries(res.headers).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => responseHeaders.append(key, v));
} else {
responseHeaders.set(key, value);
}
});
const { headers, statusCode, ...rest } = res;
// Handle empty responses (status codes that should not have bodies)
if ([204, 304].includes(statusCode)) {
return new Response(null, { ...rest, status: statusCode, headers: responseHeaders });
}
// Check for content-encoding header
const contentEncoding = res.headers['content-encoding'] || '';
// If no compression or HEAD/CONNECT request, return as-is
if (!contentEncoding || req.method === 'HEAD' || req.method === 'CONNECT') {
return new Response(res.body, { ...rest, status: statusCode, headers: responseHeaders });
}
// Parse and process content encodings
const codings = contentEncoding.toLowerCase().split(',').map(x => x.trim());
try {
// Get the body as an ArrayBuffer
const bodyArrayBuffer = await res.body.arrayBuffer();
// Convert to Buffer for Node.js zlib functions
let buffer = Buffer.from(bodyArrayBuffer);
// Apply decoders in order
for (const coding of codings) {
try {
if (coding === 'gzip' || coding === 'x-gzip') {
buffer = await gunzipAsync(buffer);
} else if (coding === 'deflate') {
buffer = await inflateAsync(buffer);
} else if (coding === 'br') {
buffer = await brotliDecompressAsync(buffer);
}
// Skip unknown encodings silently
} catch (decompressError) {
console.error(decompressError);
// If decompression fails, return original response
return new Response(new Uint8Array(bodyArrayBuffer), {
...rest,
status: statusCode,
headers: responseHeaders
});
}
}
if (looksLikeChunkedEncoding(buffer)) {
console.warn('⚠️ Warning: response appears to be chunked-encoded. undici may not have decoded it.');
}
// Create response with decoded data and remove content-encoding header
responseHeaders.delete('content-encoding');
return new Response(buffer, {
...rest,
status: statusCode,
headers: responseHeaders
});
} catch (bodyError) {
console.error(bodyError);
// If we can't process the body, fall back to the original response
return new Response(res.body, { ...rest, status: statusCode, headers: responseHeaders });
}
} catch (error) {
console.error(error);
// Handle any unexpected errors
return new Response(`Server error: ${error.message}`, { status: 500 });
}
}