com.jimuwd.xian.registry-proxy 1.0.31 → 1.0.33
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.
- package/dist/index.js +93 -64
- package/package.json +1 -1
- package/src/index.ts +103 -61
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import fetch from 'node-fetch';
|
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { join, resolve } from 'path';
|
|
9
9
|
import { URL } from 'url';
|
|
10
|
+
import { Readable } from "node:stream";
|
|
10
11
|
const { readFile, writeFile } = fsPromises;
|
|
11
12
|
class ConcurrencyLimiter {
|
|
12
13
|
maxConcurrency;
|
|
@@ -69,7 +70,7 @@ function resolvePath(path) {
|
|
|
69
70
|
function removeRegistryPrefix(tarballUrl, registries) {
|
|
70
71
|
const normalizedTarball = normalizeUrl(tarballUrl);
|
|
71
72
|
const normalizedRegistries = registries
|
|
72
|
-
.map(r => normalizeUrl(r.
|
|
73
|
+
.map(r => normalizeUrl(r.normalizedRegistryUrl))
|
|
73
74
|
.sort((a, b) => b.length - a.length);
|
|
74
75
|
for (const normalizedRegistry of normalizedRegistries) {
|
|
75
76
|
if (normalizedTarball.startsWith(normalizedRegistry)) {
|
|
@@ -130,7 +131,7 @@ async function loadProxyInfo(proxyConfigPath = './.registry-proxy.yml', localYar
|
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
|
-
registryMap.set(normalizedProxiedRegUrl, {
|
|
134
|
+
registryMap.set(normalizedProxiedRegUrl, { normalizedRegistryUrl: normalizedProxiedRegUrl, token });
|
|
134
135
|
}
|
|
135
136
|
const registries = Array.from(registryMap.values());
|
|
136
137
|
const https = proxyConfig.https;
|
|
@@ -139,9 +140,9 @@ async function loadProxyInfo(proxyConfigPath = './.registry-proxy.yml', localYar
|
|
|
139
140
|
}
|
|
140
141
|
export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
|
|
141
142
|
const proxyInfo = await loadProxyInfo(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
142
|
-
const
|
|
143
|
+
const registryInfos = proxyInfo.registries;
|
|
143
144
|
const basePathPrefixedWithSlash = removeEndingSlashAndForceStartingSlash(proxyInfo.basePath);
|
|
144
|
-
console.log('Active registries:',
|
|
145
|
+
console.log('Active registries:', registryInfos.map(r => r.normalizedRegistryUrl));
|
|
145
146
|
console.log('Proxy base path:', basePathPrefixedWithSlash);
|
|
146
147
|
console.log('HTTPS:', !!proxyInfo.https);
|
|
147
148
|
let proxyPort;
|
|
@@ -160,78 +161,105 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
160
161
|
}
|
|
161
162
|
const relativePathPrefixedWithSlash = basePathPrefixedWithSlash === '/' ? fullUrl.pathname : fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
|
|
162
163
|
console.log(`Proxying: ${relativePathPrefixedWithSlash}`);
|
|
163
|
-
|
|
164
|
+
// 修改为按顺序尝试注册表,找到第一个成功响应即返回
|
|
165
|
+
for (const { normalizedRegistryUrl, token } of registryInfos) {
|
|
164
166
|
await limiter.acquire();
|
|
167
|
+
let response = null;
|
|
165
168
|
try {
|
|
166
|
-
const
|
|
167
|
-
const targetUrl = `${url}/${cleanRelativePath}${fullUrl.search || ''}`;
|
|
169
|
+
const targetUrl = `${normalizedRegistryUrl}${relativePathPrefixedWithSlash}${fullUrl.search || ''}`;
|
|
168
170
|
console.log(`Fetching from: ${targetUrl}`);
|
|
169
171
|
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
|
|
170
|
-
|
|
172
|
+
response = await fetch(targetUrl, { headers });
|
|
171
173
|
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
174
|
+
if (response.ok) {
|
|
175
|
+
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
176
|
+
if (contentType.includes('application/json')) {
|
|
177
|
+
// application/json 元数据
|
|
178
|
+
try {
|
|
179
|
+
const data = await response.json();
|
|
180
|
+
if (data.versions) {
|
|
181
|
+
const requestHeadersHostFromYarnClient = req.headers.host || 'localhost:' + proxyPort;
|
|
182
|
+
console.log("Request headers.host from yarn client is", requestHeadersHostFromYarnClient);
|
|
183
|
+
const proxyBaseUrlNoSuffixedWithSlash = `${proxyInfo.https ? 'https' : 'http'}://${requestHeadersHostFromYarnClient}${basePathPrefixedWithSlash === '/' ? '' : basePathPrefixedWithSlash}`;
|
|
184
|
+
console.log("proxyBaseUrlNoSuffixedWithSlash", proxyBaseUrlNoSuffixedWithSlash);
|
|
185
|
+
for (const version in data.versions) {
|
|
186
|
+
const dist = data.versions[version]?.dist;
|
|
187
|
+
if (dist?.tarball) {
|
|
188
|
+
const originalUrl = new URL(dist.tarball);
|
|
189
|
+
const originalSearchParamsStr = originalUrl.search || '';
|
|
190
|
+
const tarballPathPrefixedWithSlash = removeRegistryPrefix(dist.tarball, registryInfos);
|
|
191
|
+
dist.tarball = `${proxyBaseUrlNoSuffixedWithSlash}${tarballPathPrefixedWithSlash}${originalSearchParamsStr}`;
|
|
192
|
+
if (!tarballPathPrefixedWithSlash.startsWith("/"))
|
|
193
|
+
console.error("bad tarballPath, must be PrefixedWithSlash", tarballPathPrefixedWithSlash);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
198
|
+
res.end(JSON.stringify(data));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
console.error('Failed to parse JSON response:', e);
|
|
203
|
+
// 继续尝试下一个注册表
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
if (!response.body) {
|
|
208
|
+
console.error(`Empty response body from ${response.url}, status: ${response.status}`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// 设置正确的响应头
|
|
212
|
+
const contentLength = response.headers.get('Content-Length');
|
|
213
|
+
const safeHeaders = {
|
|
214
|
+
'content-type': contentType,
|
|
215
|
+
'connection': 'keep-alive'
|
|
216
|
+
};
|
|
217
|
+
if (contentLength && !isNaN(Number(contentLength))) {
|
|
218
|
+
safeHeaders['content-length'] = contentLength;
|
|
219
|
+
}
|
|
220
|
+
// 复制其他可能有用的头信息
|
|
221
|
+
const headersToCopy = ['cache-control', 'etag', 'last-modified'];
|
|
222
|
+
headersToCopy.forEach(header => {
|
|
223
|
+
const value = response?.headers.get(header);
|
|
224
|
+
if (value) {
|
|
225
|
+
safeHeaders[header] = value;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
res.writeHead(response.status, safeHeaders);
|
|
229
|
+
// 使用 pipeline 处理流
|
|
230
|
+
const { pipeline } = await import('stream');
|
|
231
|
+
const { promisify } = await import('util');
|
|
232
|
+
const pipelineAsync = promisify(pipeline);
|
|
233
|
+
try {
|
|
234
|
+
// 将 ReadableStream 转换为 Node.js Readable
|
|
235
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
236
|
+
// 双向终止保护
|
|
237
|
+
req.on('close', () => nodeStream.destroy());
|
|
238
|
+
res.on('close', () => nodeStream.destroy());
|
|
239
|
+
await pipelineAsync(nodeStream, res);
|
|
240
|
+
return; // 成功完成管道传输后返回
|
|
241
|
+
}
|
|
242
|
+
catch (pipeError) {
|
|
243
|
+
console.error(`Stream pipeline error for ${relativePathPrefixedWithSlash}:`, pipeError);
|
|
244
|
+
if (!res.headersSent) {
|
|
245
|
+
res.writeHead(502).end('Stream Error');
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
207
248
|
}
|
|
208
249
|
}
|
|
209
250
|
}
|
|
210
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
211
|
-
res.end(JSON.stringify(data));
|
|
212
251
|
}
|
|
213
252
|
catch (e) {
|
|
214
|
-
console.error(
|
|
215
|
-
res.writeHead(502).end('Invalid Upstream Response');
|
|
253
|
+
console.error(`Failed to fetch from ${normalizedRegistryUrl}:`, e);
|
|
216
254
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
console.error(`Empty response body from ${successResponse.url}, status: ${successResponse.status}`);
|
|
221
|
-
res.writeHead(502).end('Empty Response Body');
|
|
222
|
-
return;
|
|
255
|
+
finally {
|
|
256
|
+
// 不需要手动销毁 ReadableStream
|
|
257
|
+
limiter.release();
|
|
223
258
|
}
|
|
224
|
-
const contentLength = successResponse.headers.get('Content-Length');
|
|
225
|
-
const safeHeaders = {};
|
|
226
|
-
safeHeaders["content-type"] = contentType;
|
|
227
|
-
if (contentLength && !isNaN(Number(contentLength)))
|
|
228
|
-
safeHeaders["content-length"] = contentLength;
|
|
229
|
-
res.writeHead(successResponse.status, safeHeaders);
|
|
230
|
-
successResponse.body.pipe(res).on('error', (err) => {
|
|
231
|
-
console.error(`Stream error for ${relativePathPrefixedWithSlash}:`, err);
|
|
232
|
-
res.writeHead(502).end('Stream Error');
|
|
233
|
-
});
|
|
234
259
|
}
|
|
260
|
+
// 所有注册表都尝试失败
|
|
261
|
+
console.error(`All registries failed for ${relativePathPrefixedWithSlash}`);
|
|
262
|
+
res.writeHead(404).end('Not Found - All upstream registries failed');
|
|
235
263
|
};
|
|
236
264
|
let server;
|
|
237
265
|
if (proxyInfo.https) {
|
|
@@ -255,7 +283,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
255
283
|
else {
|
|
256
284
|
server = createServer(requestHandler);
|
|
257
285
|
}
|
|
258
|
-
|
|
286
|
+
const promisedServer = new Promise((resolve, reject) => {
|
|
259
287
|
server.on('error', (err) => {
|
|
260
288
|
if (err.code === 'EADDRINUSE') {
|
|
261
289
|
console.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
@@ -273,6 +301,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
273
301
|
resolve(server);
|
|
274
302
|
});
|
|
275
303
|
});
|
|
304
|
+
return promisedServer;
|
|
276
305
|
}
|
|
277
306
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
278
307
|
const [, , configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import fetch, {Response} from 'node-fetch';
|
|
|
8
8
|
import {homedir} from 'os';
|
|
9
9
|
import {join, resolve} from 'path';
|
|
10
10
|
import {URL} from 'url';
|
|
11
|
+
import {Readable} from "node:stream";
|
|
11
12
|
|
|
12
13
|
const {readFile, writeFile} = fsPromises;
|
|
13
14
|
|
|
@@ -31,7 +32,7 @@ interface YarnConfig {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
interface RegistryInfo {
|
|
34
|
-
|
|
35
|
+
normalizedRegistryUrl: string;
|
|
35
36
|
token?: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -111,7 +112,7 @@ function resolvePath(path: string): string {
|
|
|
111
112
|
function removeRegistryPrefix(tarballUrl: string, registries: RegistryInfo[]): string {
|
|
112
113
|
const normalizedTarball = normalizeUrl(tarballUrl);
|
|
113
114
|
const normalizedRegistries = registries
|
|
114
|
-
.map(r => normalizeUrl(r.
|
|
115
|
+
.map(r => normalizeUrl(r.normalizedRegistryUrl))
|
|
115
116
|
.sort((a, b) => b.length - a.length);
|
|
116
117
|
for (const normalizedRegistry of normalizedRegistries) {
|
|
117
118
|
if (normalizedTarball.startsWith(normalizedRegistry)) {
|
|
@@ -177,7 +178,7 @@ async function loadProxyInfo(
|
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
|
-
registryMap.set(normalizedProxiedRegUrl, {
|
|
181
|
+
registryMap.set(normalizedProxiedRegUrl, {normalizedRegistryUrl: normalizedProxiedRegUrl, token});
|
|
181
182
|
}
|
|
182
183
|
const registries = Array.from(registryMap.values());
|
|
183
184
|
const https = proxyConfig.https;
|
|
@@ -192,10 +193,10 @@ export async function startProxyServer(
|
|
|
192
193
|
port: number = 0
|
|
193
194
|
): Promise<HttpServer | HttpsServer> {
|
|
194
195
|
const proxyInfo = await loadProxyInfo(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
195
|
-
const
|
|
196
|
+
const registryInfos = proxyInfo.registries;
|
|
196
197
|
const basePathPrefixedWithSlash: string = removeEndingSlashAndForceStartingSlash(proxyInfo.basePath);
|
|
197
198
|
|
|
198
|
-
console.log('Active registries:',
|
|
199
|
+
console.log('Active registries:', registryInfos.map(r => r.normalizedRegistryUrl));
|
|
199
200
|
console.log('Proxy base path:', basePathPrefixedWithSlash);
|
|
200
201
|
console.log('HTTPS:', !!proxyInfo.https);
|
|
201
202
|
|
|
@@ -219,74 +220,113 @@ export async function startProxyServer(
|
|
|
219
220
|
const relativePathPrefixedWithSlash = basePathPrefixedWithSlash === '/' ? fullUrl.pathname : fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
|
|
220
221
|
console.log(`Proxying: ${relativePathPrefixedWithSlash}`);
|
|
221
222
|
|
|
222
|
-
|
|
223
|
+
// 修改为按顺序尝试注册表,找到第一个成功响应即返回
|
|
224
|
+
for (const {normalizedRegistryUrl, token} of registryInfos) {
|
|
223
225
|
await limiter.acquire();
|
|
226
|
+
let response: Response | null = null;
|
|
224
227
|
try {
|
|
225
|
-
const
|
|
226
|
-
const targetUrl = `${url}/${cleanRelativePath}${fullUrl.search || ''}`;
|
|
228
|
+
const targetUrl = `${normalizedRegistryUrl}${relativePathPrefixedWithSlash}${fullUrl.search || ''}`;
|
|
227
229
|
console.log(`Fetching from: ${targetUrl}`);
|
|
228
230
|
const headers = token ? {Authorization: `Bearer ${token}`} : undefined;
|
|
229
|
-
|
|
231
|
+
response = await fetch(targetUrl, {headers});
|
|
230
232
|
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
231
|
-
return response.ok ? response : null;
|
|
232
|
-
} catch (e) {
|
|
233
|
-
console.error(`Failed to fetch from ${url}:`, e);
|
|
234
|
-
return null;
|
|
235
|
-
} finally {
|
|
236
|
-
limiter.release();
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
233
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
234
|
+
if (response.ok) {
|
|
235
|
+
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
236
|
+
|
|
237
|
+
if (contentType.includes('application/json')) {
|
|
238
|
+
// application/json 元数据
|
|
239
|
+
try {
|
|
240
|
+
const data = await response.json() as PackageData;
|
|
241
|
+
if (data.versions) {
|
|
242
|
+
const requestHeadersHostFromYarnClient = req.headers.host || 'localhost:' + proxyPort;
|
|
243
|
+
console.log("Request headers.host from yarn client is", requestHeadersHostFromYarnClient);
|
|
244
|
+
const proxyBaseUrlNoSuffixedWithSlash = `${proxyInfo.https ? 'https' : 'http'}://${requestHeadersHostFromYarnClient}${basePathPrefixedWithSlash === '/' ? '' : basePathPrefixedWithSlash}`;
|
|
245
|
+
console.log("proxyBaseUrlNoSuffixedWithSlash", proxyBaseUrlNoSuffixedWithSlash);
|
|
246
|
+
for (const version in data.versions) {
|
|
247
|
+
const dist = data.versions[version]?.dist;
|
|
248
|
+
if (dist?.tarball) {
|
|
249
|
+
const originalUrl = new URL(dist.tarball);
|
|
250
|
+
const originalSearchParamsStr = originalUrl.search || '';
|
|
251
|
+
const tarballPathPrefixedWithSlash = removeRegistryPrefix(dist.tarball, registryInfos);
|
|
252
|
+
dist.tarball = `${proxyBaseUrlNoSuffixedWithSlash}${tarballPathPrefixedWithSlash}${originalSearchParamsStr}`;
|
|
253
|
+
if (!tarballPathPrefixedWithSlash.startsWith("/")) console.error("bad tarballPath, must be PrefixedWithSlash", tarballPathPrefixedWithSlash);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
258
|
+
res.end(JSON.stringify(data));
|
|
259
|
+
return;
|
|
260
|
+
} catch (e) {
|
|
261
|
+
console.error('Failed to parse JSON response:', e);
|
|
262
|
+
// 继续尝试下一个注册表
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
if (!response.body) {
|
|
266
|
+
console.error(`Empty response body from ${response.url}, status: ${response.status}`);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
247
269
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
270
|
+
// 设置正确的响应头
|
|
271
|
+
const contentLength = response.headers.get('Content-Length');
|
|
272
|
+
const safeHeaders: OutgoingHttpHeaders = {
|
|
273
|
+
'content-type': contentType,
|
|
274
|
+
'connection': 'keep-alive'
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (contentLength && !isNaN(Number(contentLength))) {
|
|
278
|
+
safeHeaders['content-length'] = contentLength;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 复制其他可能有用的头信息
|
|
282
|
+
const headersToCopy = ['cache-control', 'etag', 'last-modified'];
|
|
283
|
+
headersToCopy.forEach(header => {
|
|
284
|
+
const value = response?.headers.get(header);
|
|
285
|
+
if (value) {
|
|
286
|
+
safeHeaders[header] = value;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
res.writeHead(response.status, safeHeaders);
|
|
291
|
+
|
|
292
|
+
// 使用 pipeline 处理流
|
|
293
|
+
const {pipeline} = await import('stream');
|
|
294
|
+
const {promisify} = await import('util');
|
|
295
|
+
const pipelineAsync = promisify(pipeline);
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
// 将 ReadableStream 转换为 Node.js Readable
|
|
299
|
+
const nodeStream = Readable.fromWeb(response.body as any);
|
|
300
|
+
|
|
301
|
+
// 双向终止保护
|
|
302
|
+
req.on('close', () => nodeStream.destroy());
|
|
303
|
+
res.on('close', () => nodeStream.destroy());
|
|
304
|
+
|
|
305
|
+
await pipelineAsync(
|
|
306
|
+
nodeStream,
|
|
307
|
+
res
|
|
308
|
+
);
|
|
309
|
+
return; // 成功完成管道传输后返回
|
|
310
|
+
} catch (pipeError) {
|
|
311
|
+
console.error(`Stream pipeline error for ${relativePathPrefixedWithSlash}:`, pipeError);
|
|
312
|
+
if (!res.headersSent) {
|
|
313
|
+
res.writeHead(502).end('Stream Error');
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
265
316
|
}
|
|
266
317
|
}
|
|
267
318
|
}
|
|
268
|
-
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
269
|
-
res.end(JSON.stringify(data));
|
|
270
319
|
} catch (e) {
|
|
271
|
-
console.error(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (!successResponse.body) {
|
|
276
|
-
console.error(`Empty response body from ${successResponse.url}, status: ${successResponse.status}`);
|
|
277
|
-
res.writeHead(502).end('Empty Response Body');
|
|
278
|
-
return;
|
|
320
|
+
console.error(`Failed to fetch from ${normalizedRegistryUrl}:`, e);
|
|
321
|
+
} finally {
|
|
322
|
+
// 不需要手动销毁 ReadableStream
|
|
323
|
+
limiter.release();
|
|
279
324
|
}
|
|
280
|
-
const contentLength = successResponse.headers.get('Content-Length');
|
|
281
|
-
const safeHeaders: OutgoingHttpHeaders = {};
|
|
282
|
-
safeHeaders["content-type"] = contentType;
|
|
283
|
-
if (contentLength && !isNaN(Number(contentLength))) safeHeaders["content-length"] = contentLength;
|
|
284
|
-
res.writeHead(successResponse.status, safeHeaders);
|
|
285
|
-
successResponse.body.pipe(res).on('error', (err: any) => {
|
|
286
|
-
console.error(`Stream error for ${relativePathPrefixedWithSlash}:`, err);
|
|
287
|
-
res.writeHead(502).end('Stream Error');
|
|
288
|
-
});
|
|
289
325
|
}
|
|
326
|
+
|
|
327
|
+
// 所有注册表都尝试失败
|
|
328
|
+
console.error(`All registries failed for ${relativePathPrefixedWithSlash}`);
|
|
329
|
+
res.writeHead(404).end('Not Found - All upstream registries failed');
|
|
290
330
|
};
|
|
291
331
|
|
|
292
332
|
let server: HttpServer | HttpsServer;
|
|
@@ -310,7 +350,7 @@ export async function startProxyServer(
|
|
|
310
350
|
server = createServer(requestHandler);
|
|
311
351
|
}
|
|
312
352
|
|
|
313
|
-
|
|
353
|
+
const promisedServer: Promise<HttpServer | HttpsServer> = new Promise((resolve, reject) => {
|
|
314
354
|
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
315
355
|
if (err.code === 'EADDRINUSE') {
|
|
316
356
|
console.error(`Port ${port} is in use, please specify a different port or free it.`);
|
|
@@ -328,6 +368,8 @@ export async function startProxyServer(
|
|
|
328
368
|
resolve(server);
|
|
329
369
|
});
|
|
330
370
|
});
|
|
371
|
+
|
|
372
|
+
return promisedServer as Promise<HttpServer | HttpsServer>;
|
|
331
373
|
}
|
|
332
374
|
|
|
333
375
|
if (import.meta.url === `file://${process.argv[1]}`) {
|