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 onStatusCode(
    1. int code
    )?,
  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.
  • onStatusCode if specified, it'll be called with the status code received.
  • 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 onStatusCode(int code)?,
      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;
    onStatusCode?.call(code);

    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 copyToSink(clientResponse.stream, serverResponse,
        closeSink: true);
  } finally {
    client.close();
  }
}