com.jimuwd.xian.registry-proxy 1.0.33 → 1.0.35
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 +74 -102
- package/package.json +1 -1
- package/src/index.ts +99 -110
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { homedir } from 'os';
|
|
|
8
8
|
import { join, resolve } from 'path';
|
|
9
9
|
import { URL } from 'url';
|
|
10
10
|
import { Readable } from "node:stream";
|
|
11
|
+
import { pipeline } from "node:stream/promises";
|
|
11
12
|
const { readFile, writeFile } = fsPromises;
|
|
12
13
|
class ConcurrencyLimiter {
|
|
13
14
|
maxConcurrency;
|
|
@@ -138,6 +139,60 @@ async function loadProxyInfo(proxyConfigPath = './.registry-proxy.yml', localYar
|
|
|
138
139
|
const basePath = removeEndingSlashAndForceStartingSlash(proxyConfig.basePath);
|
|
139
140
|
return { registries, https, basePath };
|
|
140
141
|
}
|
|
142
|
+
async function fetchFromRegistry(registry, path, search, limiter) {
|
|
143
|
+
await limiter.acquire();
|
|
144
|
+
try {
|
|
145
|
+
const targetUrl = `${registry.normalizedRegistryUrl}${path}${search || ''}`;
|
|
146
|
+
console.log(`Fetching from: ${targetUrl}`);
|
|
147
|
+
const headers = registry.token ? { Authorization: `Bearer ${registry.token}` } : undefined;
|
|
148
|
+
const response = await fetch(targetUrl, { headers });
|
|
149
|
+
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
150
|
+
return response.ok ? response : null;
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
if (e instanceof Error) {
|
|
154
|
+
console.error(e.code === 'ECONNREFUSED'
|
|
155
|
+
? `Registry ${registry.normalizedRegistryUrl} unreachable [ECONNREFUSED]`
|
|
156
|
+
: `Error from ${registry.normalizedRegistryUrl}: ${e.message}`);
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
limiter.release();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function writeResponse(res, response, req, proxyInfo, proxyPort, registryInfos) {
|
|
165
|
+
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
166
|
+
if (contentType.includes('application/json')) {
|
|
167
|
+
const data = await response.json();
|
|
168
|
+
if (data.versions) {
|
|
169
|
+
const host = req.headers.host || `localhost:${proxyPort}`;
|
|
170
|
+
const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
|
|
171
|
+
for (const version in data.versions) {
|
|
172
|
+
const tarball = data.versions[version]?.dist?.tarball;
|
|
173
|
+
if (tarball) {
|
|
174
|
+
const path = removeRegistryPrefix(tarball, registryInfos);
|
|
175
|
+
data.versions[version].dist.tarball = `${baseUrl}${path}${new URL(tarball).search || ''}`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
180
|
+
res.end(JSON.stringify(data));
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const safeHeaders = {
|
|
184
|
+
'content-type': contentType,
|
|
185
|
+
'connection': 'keep-alive',
|
|
186
|
+
};
|
|
187
|
+
['cache-control', 'etag', 'last-modified', 'content-encoding'].forEach(header => {
|
|
188
|
+
const value = response.headers.get(header);
|
|
189
|
+
if (value)
|
|
190
|
+
safeHeaders[header] = value;
|
|
191
|
+
});
|
|
192
|
+
res.writeHead(response.status, safeHeaders);
|
|
193
|
+
await pipeline(Readable.fromWeb(response.body), res);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
141
196
|
export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
|
|
142
197
|
const proxyInfo = await loadProxyInfo(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
143
198
|
const registryInfos = proxyInfo.registries;
|
|
@@ -148,118 +203,35 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
148
203
|
let proxyPort;
|
|
149
204
|
const requestHandler = async (req, res) => {
|
|
150
205
|
if (!req.url || !req.headers.host) {
|
|
151
|
-
console.error('Invalid request: missing URL or host header');
|
|
152
206
|
res.writeHead(400).end('Invalid Request');
|
|
153
207
|
return;
|
|
154
208
|
}
|
|
155
209
|
const fullUrl = new URL(req.url, `${proxyInfo.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
156
|
-
console.log(`Proxy server received request on ${fullUrl.toString()}`);
|
|
157
210
|
if (!fullUrl.pathname.startsWith(basePathPrefixedWithSlash)) {
|
|
158
|
-
console.error(`Path ${fullUrl.pathname} does not match basePath ${basePathPrefixedWithSlash}`);
|
|
159
211
|
res.writeHead(404).end('Not Found');
|
|
160
212
|
return;
|
|
161
213
|
}
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
catch (e) {
|
|
253
|
-
console.error(`Failed to fetch from ${normalizedRegistryUrl}:`, e);
|
|
254
|
-
}
|
|
255
|
-
finally {
|
|
256
|
-
// 不需要手动销毁 ReadableStream
|
|
257
|
-
limiter.release();
|
|
214
|
+
const path = basePathPrefixedWithSlash === '/'
|
|
215
|
+
? fullUrl.pathname
|
|
216
|
+
: fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
|
|
217
|
+
// 顺序尝试注册表,获取第一个成功响应
|
|
218
|
+
let successfulResponse = null;
|
|
219
|
+
for (const registry of registryInfos) {
|
|
220
|
+
if (req.destroyed)
|
|
221
|
+
break;
|
|
222
|
+
const response = await fetchFromRegistry(registry, path, fullUrl.search, limiter);
|
|
223
|
+
if (response) {
|
|
224
|
+
successfulResponse = response;
|
|
225
|
+
break;
|
|
258
226
|
}
|
|
259
227
|
}
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
228
|
+
// 统一回写响应
|
|
229
|
+
if (successfulResponse) {
|
|
230
|
+
await writeResponse(res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
res.writeHead(404).end('All upstream registries failed');
|
|
234
|
+
}
|
|
263
235
|
};
|
|
264
236
|
let server;
|
|
265
237
|
if (proxyInfo.https) {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {homedir} from 'os';
|
|
|
9
9
|
import {join, resolve} from 'path';
|
|
10
10
|
import {URL} from 'url';
|
|
11
11
|
import {Readable} from "node:stream";
|
|
12
|
+
import {pipeline} from "node:stream/promises";
|
|
12
13
|
|
|
13
14
|
const {readFile, writeFile} = fsPromises;
|
|
14
15
|
|
|
@@ -186,6 +187,79 @@ async function loadProxyInfo(
|
|
|
186
187
|
return {registries, https, basePath};
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
async function fetchFromRegistry(
|
|
191
|
+
registry: RegistryInfo,
|
|
192
|
+
path: string,
|
|
193
|
+
search: string,
|
|
194
|
+
limiter: ConcurrencyLimiter
|
|
195
|
+
): Promise<Response | null> {
|
|
196
|
+
await limiter.acquire();
|
|
197
|
+
try {
|
|
198
|
+
const targetUrl = `${registry.normalizedRegistryUrl}${path}${search || ''}`;
|
|
199
|
+
console.log(`Fetching from: ${targetUrl}`);
|
|
200
|
+
const headers = registry.token ? { Authorization: `Bearer ${registry.token}` } : undefined;
|
|
201
|
+
const response = await fetch(targetUrl, { headers });
|
|
202
|
+
console.log(`Response from ${targetUrl}: ${response.status} ${response.statusText}`);
|
|
203
|
+
return response.ok ? response : null;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
if (e instanceof Error) {
|
|
206
|
+
console.error(
|
|
207
|
+
(e as any).code === 'ECONNREFUSED'
|
|
208
|
+
? `Registry ${registry.normalizedRegistryUrl} unreachable [ECONNREFUSED]`
|
|
209
|
+
: `Error from ${registry.normalizedRegistryUrl}: ${e.message}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
} finally {
|
|
214
|
+
limiter.release();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function writeResponse(
|
|
219
|
+
res: ServerResponse,
|
|
220
|
+
response: Response,
|
|
221
|
+
req: IncomingMessage,
|
|
222
|
+
proxyInfo: ProxyInfo,
|
|
223
|
+
proxyPort: number,
|
|
224
|
+
registryInfos: RegistryInfo[]
|
|
225
|
+
): Promise<void> {
|
|
226
|
+
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
|
227
|
+
|
|
228
|
+
if (contentType.includes('application/json')) {
|
|
229
|
+
const data = await response.json() as PackageData;
|
|
230
|
+
if (data.versions) {
|
|
231
|
+
const host = req.headers.host || `localhost:${proxyPort}`;
|
|
232
|
+
const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
|
|
233
|
+
|
|
234
|
+
for (const version in data.versions) {
|
|
235
|
+
const tarball = data.versions[version]?.dist?.tarball;
|
|
236
|
+
if (tarball) {
|
|
237
|
+
const path = removeRegistryPrefix(tarball, registryInfos);
|
|
238
|
+
data.versions[version]!.dist!.tarball = `${baseUrl}${path}${new URL(tarball).search || ''}`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
243
|
+
res.end(JSON.stringify(data));
|
|
244
|
+
} else {
|
|
245
|
+
const safeHeaders: OutgoingHttpHeaders = {
|
|
246
|
+
'content-type': contentType,
|
|
247
|
+
'connection': 'keep-alive',
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
['cache-control', 'etag', 'last-modified', 'content-encoding'].forEach(header => {
|
|
251
|
+
const value = response.headers.get(header);
|
|
252
|
+
if (value) safeHeaders[header] = value;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
res.writeHead(response.status, safeHeaders);
|
|
256
|
+
await pipeline(
|
|
257
|
+
Readable.fromWeb(response.body as any),
|
|
258
|
+
res
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
189
263
|
export async function startProxyServer(
|
|
190
264
|
proxyConfigPath?: string,
|
|
191
265
|
localYarnConfigPath?: string,
|
|
@@ -204,129 +278,44 @@ export async function startProxyServer(
|
|
|
204
278
|
|
|
205
279
|
const requestHandler = async (req: IncomingMessage, res: ServerResponse) => {
|
|
206
280
|
if (!req.url || !req.headers.host) {
|
|
207
|
-
console.error('Invalid request: missing URL or host header');
|
|
208
281
|
res.writeHead(400).end('Invalid Request');
|
|
209
282
|
return;
|
|
210
283
|
}
|
|
211
284
|
|
|
212
285
|
const fullUrl = new URL(req.url, `${proxyInfo.https ? 'https' : 'http'}://${req.headers.host}`);
|
|
213
|
-
console.log(`Proxy server received request on ${fullUrl.toString()}`)
|
|
214
286
|
if (!fullUrl.pathname.startsWith(basePathPrefixedWithSlash)) {
|
|
215
|
-
console.error(`Path ${fullUrl.pathname} does not match basePath ${basePathPrefixedWithSlash}`);
|
|
216
287
|
res.writeHead(404).end('Not Found');
|
|
217
288
|
return;
|
|
218
289
|
}
|
|
219
290
|
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
}
|
|
269
|
-
|
|
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;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
} catch (e) {
|
|
320
|
-
console.error(`Failed to fetch from ${normalizedRegistryUrl}:`, e);
|
|
321
|
-
} finally {
|
|
322
|
-
// 不需要手动销毁 ReadableStream
|
|
323
|
-
limiter.release();
|
|
291
|
+
const path = basePathPrefixedWithSlash === '/'
|
|
292
|
+
? fullUrl.pathname
|
|
293
|
+
: fullUrl.pathname.slice(basePathPrefixedWithSlash.length);
|
|
294
|
+
|
|
295
|
+
// 顺序尝试注册表,获取第一个成功响应
|
|
296
|
+
let successfulResponse: Response | null = null;
|
|
297
|
+
for (const registry of registryInfos) {
|
|
298
|
+
if (req.destroyed) break;
|
|
299
|
+
|
|
300
|
+
const response = await fetchFromRegistry(
|
|
301
|
+
registry,
|
|
302
|
+
path,
|
|
303
|
+
fullUrl.search,
|
|
304
|
+
limiter
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (response) {
|
|
308
|
+
successfulResponse = response;
|
|
309
|
+
break;
|
|
324
310
|
}
|
|
325
311
|
}
|
|
326
312
|
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
313
|
+
// 统一回写响应
|
|
314
|
+
if (successfulResponse) {
|
|
315
|
+
await writeResponse(res, successfulResponse, req, proxyInfo, proxyPort, registryInfos);
|
|
316
|
+
} else {
|
|
317
|
+
res.writeHead(404).end('All upstream registries failed');
|
|
318
|
+
}
|
|
330
319
|
};
|
|
331
320
|
|
|
332
321
|
let server: HttpServer | HttpsServer;
|