Skip to content

[Duplicate Code] Near-identical error-handling blocks in proxy-request.js repeated 3 times #3306

Description

@github-actions

Duplicate Code Opportunity

Summary

  • Pattern: Three nearly-identical error-handling + metrics + logging blocks inside proxyRequest() and proxyWebSocket() in containers/api-proxy/proxy-request.js
  • Locations: Lines 201–213, 358–367, 421–433 of containers/api-proxy/proxy-request.js
  • Impact: ~36 lines of near-duplicate error handling; any change to the log schema or metrics API must be made in three places, creating drift risk in a security-relevant logging path

Evidence

Block 1 — client request error (lines 201–213):

req.on('error', (err) => {
  if (errored) return;
  errored = true;
  const duration = Date.now() - startTime;
  metrics.gaugeDec('active_requests', { provider });
  metrics.increment('requests_errors_total', { provider });
  logRequest('error', 'request_error', {
    request_id: requestId, provider, method: req.method,
    path: sanitizeForLog(req.url), duration_ms: duration,
    error: sanitizeForLog(err.message), upstream_host: targetHost,
  });
  if (!res.headersSent) res.writeHead(400, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ error: 'Client error', message: err.message }));
});

Block 2 — proxy response stream error (lines 358–368):

proxyRes.on('error', (err) => {
  const duration = Date.now() - startTime;
  metrics.gaugeDec('active_requests', { provider });
  metrics.increment('requests_errors_total', { provider });
  logRequest('error', 'request_error', {
    request_id: requestId, provider, method: req.method,
    path: sanitizeForLog(req.url), duration_ms: duration,
    error: sanitizeForLog(err.message), upstream_host: targetHost,
  });
  if (!res.headersSent) res.writeHead(502, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ error: 'Response stream error', message: err.message }));
});

Block 3 — upstream proxy request error (lines 421–433):

proxyReq.on('error', (err) => {
  const duration = Date.now() - startTime;
  metrics.gaugeDec('active_requests', { provider });
  metrics.increment('requests_errors_total', { provider });
  metrics.increment('requests_total', { provider, method: req.method, status_class: '5xx' });
  metrics.observe('request_duration_ms', duration, { provider });
  logRequest('error', 'request_error', {
    request_id: requestId, provider, method: req.method,
    path: sanitizeForLog(req.url), duration_ms: duration,
    error: sanitizeForLog(err.message), upstream_host: targetHost,
  });
  if (!res.headersSent) res.writeHead(502, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ error: 'Proxy error', message: err.message }));
});

Suggested Refactoring

Extract a helper function in containers/api-proxy/proxy-request.js:

function handleRequestError(err, { res, requestId, provider, req, targetHost, startTime, statusCode, clientMessage, extraMetrics }) {
  const duration = Date.now() - startTime;
  metrics.gaugeDec('active_requests', { provider });
  metrics.increment('requests_errors_total', { provider });
  if (extraMetrics) extraMetrics(duration);
  logRequest('error', 'request_error', {
    request_id: requestId, provider, method: req.method,
    path: sanitizeForLog(req.url), duration_ms: duration,
    error: sanitizeForLog(err.message), upstream_host: targetHost,
  });
  if (!res.headersSent) res.writeHead(statusCode, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ error: clientMessage, message: err.message }));
}

Each error handler then becomes a one-liner call. This is especially important because the logRequest call is on the security audit logging path — fields logged here appear in the firewall's access audit trail.

Affected Files

  • containers/api-proxy/proxy-request.js — lines 201–213, 358–367, 421–433

Effort Estimate

Low — pure internal refactor within one file, well-covered by existing server.proxy.test.js tests.


Detected by Duplicate Code Detector workflow. Run date: 2026-05-17

Generated by Duplicate Code Detector · ● 8M ·

  • expires on Jun 16, 2026, 9:58 PM UTC

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions