proxy function

Future<Response> proxy(
  1. Context c,
  2. String url, {
  3. ProxyOptions? options,
})

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-Encoding header is managed internally so that HttpClient can decompress the upstream response transparently.
  • Content-Encoding and Content-Length are 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);
  }
}