proxyRequest function

Future proxyRequest(
  1. HttpConnect connect,
  2. dynamic url, {
  3. String? proxyName,
  4. FutureOr<bool> shallRetry(
    1. dynamic ex,
    2. StackTrace st
    )?,
  5. 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.
  • 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(ex, StackTrace st)?,
      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;
  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
        _Listener<List<int>>? copyTo;
        if (shallRetry != null) {
          final body = requestBody = <int>[];
          copyTo = (List<int> event) {
            body.addAll(event);
            clientRequest.sink.add(event);
          };
        }

        await _copy(serverRequest, clientRequest.sink, copyTo: copyTo);
      } else { //retries
        clientRequest.sink.add(requestBody);
      }

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

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

  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) => '${m[1]}${Uri.encodeComponent(m[2]!)}${m[3]}');
        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);
      }
    }
  }

  await _copy(clientResponse.stream, serverResponse);
}