proxyRequest function

Future proxyRequest(
  1. HttpConnect connect,
  2. dynamic url, {
  3. String? proxyName,
  4. FutureOr<bool> shallRetry(
    1. Object ex,
    2. StackTrace st
    )?,
  5. void onResponseHeaders(
    1. int statusCode,
    2. HttpHeaders headers
    )?,
  6. void log(
    1. String errmsg
    )?,
})

Proxies the request of url to connect.

Example:

Future proxyFoo(HttpConnect connect)
=> proxy(connect, getTargetUrl(connect));
  • url must be a String or Uri.
  • proxyName is used in headers to identify this proxy. It should be a valid HTTP token or a hostname. It defaults to null -- no via header will be added.
  • shallRetry a callback to decide whether to retry when proxyRequest receives an exception. Ignored if omitted.
  • onResponseHeaders if specified, it'll be called after the upstream response headers have been merged into connect's response and before the body is sent. The given HttpHeaders is the (still mutable) response headers, so the caller can inspect what the upstream returned and adjust it -- e.g., apply a default header only when the upstream didn't provide one.
  • log If specified, it'll be called if there is an ignorable error, e.g., header's value containing invalid characters

Implementation

Future proxyRequest(HttpConnect connect, url, {String? proxyName,
      FutureOr<bool> shallRetry(Object ex, StackTrace st)?,
      void onResponseHeaders(int statusCode, HttpHeaders headers)?,
      void log(String errmsg)?}) async {
  //COPRYRIGHT NOTICE:
  //The code is ported from [shelf_proxy](https://github.com/dart-lang/shelf_proxy)

  Uri uri;
  if (url is String) {
    uri = Uri.parse(url);
  } else if (url is Uri) {
    uri = url;
  } else {
    throw ArgumentError.value(url, 'url', 'url must be a String or Uri.');
  }

  final client = http.Client(),
    serverRequest = connect.request,
    serverResponse = connect.response;
  try {
    http.StreamedResponse clientResponse;

    for (List<int>? requestBody;;) {
      try {
        final clientRequest = http.StreamedRequest(serverRequest.method, uri);
        clientRequest.followRedirects = false;
        serverRequest.headers.forEach((name, values) {
          for (final value in values)
            if (Rsp.isHeaderValueValid(value))
              _addHeader(clientRequest.headers, name, value);
            else
              (log ?? _logger.warning)('Ignored: invalid request header value: $name=${json.encode(value)}');
        });
        clientRequest.headers['Host'] = uri.authority;

        // Add a Via header. See
        // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
        _addHeader(clientRequest.headers, 'via',
            '${serverRequest.protocolVersion} ${proxyName??"Stream"}');

        if (requestBody == null) { //first time
          _CopyTo<List<int>>? copyTo;
          if (shallRetry != null) {
            final body = requestBody = <int>[];
            copyTo = (List<int> event, void close()) {
              body.addAll(event);
              clientRequest.sink.add(event);
            };
          }

          await copyToSink(serverRequest, clientRequest.sink,
              copyTo: copyTo, closeSink: true);
        } else { //retries
          clientRequest.sink.add(requestBody);
          await clientRequest.sink.close();
        }

        clientResponse = await client.send(clientRequest);
        break; //done

      } catch (ex, st) {
        if (shallRetry == null || (await shallRetry(ex, st)) != true)
          rethrow;
        //retry
      }
    }

    final code = serverResponse.statusCode = clientResponse.statusCode;

    clientResponse.headers.forEach((name, value) {
      if (!Rsp.isHeaderValueValid(value)) {
        var fixed = false;
        if (name.toLowerCase() == 'content-disposition') {
          value = value.replaceAllMapped(
              RegExp(r'name="([^"]+)"'),
              (m) => 'name="${Uri.encodeComponent(m[1]!)}"');
          fixed = Rsp.isHeaderValueValid(value);
        }

        if (!fixed) {
          (log ?? _logger.warning)('Ignored: invalid response header value: $name=${json.encode(value)}');
          return; //skip
        }
      }

      serverResponse.headers.add(name, value, preserveHeaderCase: true);
    });

    // Add a Via header. See
    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
    if (proxyName != null)
      serverResponse.headers.add('via', '1.1 $proxyName');

    // Remove the transfer-encoding since the body has already been decoded by
    // [client].
    serverResponse.headers.removeAll('transfer-encoding');

    // If the original response was gzipped, it will be decoded by [client]
    // and we'll have no way of knowing its actual content-length.
    if (clientResponse.headers['content-encoding'] == 'gzip') {
      serverResponse.headers
        ..removeAll(HttpHeaders.contentEncodingHeader)
        ..removeAll(HttpHeaders.contentLengthHeader);

      // Add a Warning header. See
      // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2
      serverResponse.headers.add('warning', '214 ${proxyName??'Stream'} "GZIP decoded"');
    }

    // Make sure the Location header is pointing to the proxy server rather
    // than the destination server, if possible.
    if (clientResponse.isRedirect) {
      final rawLocation = clientResponse.headers['location'];
      if (rawLocation != null) {
        var location = uri.resolve(rawLocation).toString();
        if (p.url.isWithin(uri.toString(), location)) {
          serverResponse.headers.set('location',
              '/' + p.url.relative(location, from: uri.toString()));
        } else {
          serverResponse.headers.set('location', location);
        }
      }
    }

    // Let the caller inspect/adjust the response headers (already merged from
    // the upstream response) before the body is sent.
    onResponseHeaders?.call(code, serverResponse.headers);

    await copyToSink(clientResponse.stream, serverResponse,
        closeSink: true);
  } finally {
    client.close();
  }
}