proxy function
Forwards the current request to url and returns the upstream response as
a Darto Response.
- Hop-by-hop headers (Connection, Keep-Alive, Transfer-Encoding, etc.) are automatically stripped from both the outgoing request and the response.
- The
Accept-Encodingheader is managed internally so that HttpClient can decompress the upstream response transparently. Content-EncodingandContent-Lengthare removed from the forwarded response since the body has already been decoded.
// Transparent reverse proxy — forwards method, headers, and body
app.all('/api/*', (Context c) =>
proxy(c, 'https://backend.com${c.req.path}'));
// Override auth header and remove cookies
app.get('/data', (Context c) async =>
proxy(c, 'https://external.com/data',
options: ProxyOptions(
headers: {
'Authorization': 'Bearer INTERNAL_TOKEN',
'Cookie': null,
},
),
),
);
Implementation
Future<Response> proxy(
Context c,
String url, {
ProxyOptions? options,
}) async {
final opts = options ?? const ProxyOptions();
final method = (opts.method ?? c.req.method).toUpperCase();
final uri = Uri.parse(url);
final client = HttpClient()..autoUncompress = true;
try {
final upstreamReq = await client.openUrl(method, uri);
// ── Forward original headers ──────────────────────────────────────────────
if (opts.forwardHeaders) {
// Build a set of header keys that the override map will handle, so we
// don't set them twice.
final overrideKeys = opts.headers?.keys
.map((k) => k.toLowerCase())
.toSet() ??
const <String>{};
c.req.headers.forEach((name, value) {
if (_stripFromRequest.contains(name.toLowerCase())) return;
if (overrideKeys.contains(name.toLowerCase())) return;
upstreamReq.headers.set(name, value);
});
}
// ── Apply header overrides ────────────────────────────────────────────────
if (opts.headers != null) {
for (final entry in opts.headers!.entries) {
if (entry.value == null) {
upstreamReq.headers.removeAll(entry.key);
} else {
upstreamReq.headers.set(entry.key, entry.value!);
}
}
}
// ── Forward body ─────────────────────────────────────────────────────────
if (opts.forwardBody && _methodHasBody(method)) {
final body = await c.req.blob();
if (body.isNotEmpty) {
upstreamReq.contentLength = body.length;
upstreamReq.add(body);
}
}
// ── Send request and collect response ─────────────────────────────────────
final upstreamResp = await upstreamReq.close();
final bytes = await upstreamResp.fold<List<int>>(
[],
(buf, chunk) => buf..addAll(chunk),
);
// ── Build response headers (strip hop-by-hop) ─────────────────────────────
final respHeaders = <String, String>{};
upstreamResp.headers.forEach((name, values) {
if (!_stripFromResponse.contains(name.toLowerCase())) {
respHeaders[name] = values.join(', ');
}
});
final ct = upstreamResp.headers.contentType;
final contentType = ct?.toString() ?? 'application/octet-stream';
return Response.bytes(
bytes,
status: upstreamResp.statusCode,
contentType: contentType,
headers: respHeaders,
);
} on SocketException {
// Upstream unreachable (not running, wrong address, firewall, etc.)
return Response.json(
{
'error': 'Bad Gateway',
'message': 'Upstream service is unavailable',
'upstream': '${uri.host}:${uri.port}',
},
status: 502,
);
} on HttpException catch (e) {
// Upstream returned a malformed HTTP response
return Response.json(
{
'error': 'Bad Gateway',
'message': e.message,
'upstream': '${uri.host}:${uri.port}',
},
status: 502,
);
} finally {
client.close(force: false);
}
}