close method

  1. @override
Future<HttpClientResponse> close()
override

Close the request for input. Returns the value of done.

Implementation

@override
Future<HttpClientResponse> close() async {
  double? contextId = WebFHttpOverrides.getContextHeader(headers);
  HttpClientRequest request = this;

  // Get the loading state dumper for tracking
  final dumper = contextId != null ? LoadingStateRegistry.instance.getDumper(contextId) : null;

  // Track request start if not already tracked by NetworkBundle
  if (dumper != null && ownerBundle == null) {
    final requestHeaders = <String, String>{};
    headers.forEach((name, values) {
      requestHeaders[name] = values.join(', ');
    });
    // Check if this is a Fetch/XHR request by looking for the marker header
    final isFetchRequest = headers.value('X-WebF-Request-Type') == 'fetch';
    dumper.recordNetworkRequestStart(
      _uri.toString(),
      method: _method,
      headers: requestHeaders,
      isXHR: isFetchRequest,
      protocol: _uri.scheme,
      remotePort: _uri.hasAuthority ? _uri.port : null,
    );
  }

  if (contextId != null) {
    // Standard reference: https://datatracker.ietf.org/doc/html/rfc7231#section-5.5.2
    //   Most general-purpose user agents do not send the
    //   Referer header field when the referring resource is a local "file" or
    //   "data" URI.  A user agent MUST NOT send a Referer header field in an
    //   unsecured HTTP request if the referring page was received with a
    //   secure protocol.
    Uri referrer = getEntrypointUri(contextId);
    bool isUnsafe = referrer.isScheme('https') && !uri.isScheme('https');
    bool isLocalRequest = uri.isScheme('file') || uri.isScheme('data') || uri.isScheme('assets');
    if (!isUnsafe && !isLocalRequest) {
      headers.set(HttpHeaders.refererHeader, referrer.toString());
    }

    // Standard reference: https://fetch.spec.whatwg.org/#origin-header
    // `if request’s method is neither `GET` nor `HEAD`, then follow referrer policy to append origin.`
    // @TODO: Apply referrer policy.
    String origin = getOrigin(referrer);
    if (method != 'GET' && method != 'HEAD') {
      headers.set(_httpHeadersOrigin, origin);
    }

    // Step 1: Prepare request (no custom interceptor).
    await CookieManager.loadForRequest(_uri, request.cookies);

    // Step 2: Handle cache-control and expires,
    //        if hit, no need to open request.
    HttpCacheObject? cacheObject;
    // Per-controller cache toggle: allow a controller to override global cache mode.
    bool cacheEnabled = true;
    final ctrl = WebFController.getControllerOfJSContextId(contextId);
    final bool controllerWantsCache = ctrl?.networkOptions?.effectiveEnableHttpCache == true;
    final bool controllerForbidsCache = ctrl?.networkOptions?.effectiveEnableHttpCache == false;
    final bool globalCacheOn = HttpCacheController.mode != HttpCacheMode.NO_CACHE;
    cacheEnabled = controllerForbidsCache ? false : (controllerWantsCache ? true : globalCacheOn);

    if (cacheEnabled) {
      HttpCacheController cacheController = HttpCacheController.instance(origin);
      cacheObject = await cacheController.getCacheObject(request.uri);
      if (cacheObject.hitLocalCache(request)) {
        HttpClientResponse? cacheResponse = await cacheObject.toHttpClientResponse(_nativeHttpClient);
        ownerBundle?.setLoadingFromCache();
        if (cacheResponse != null) {
          // Track cache hit
          dumper?.recordNetworkRequestCacheInfo(_uri.toString(),
            cacheHit: true,
            cacheType: 'disk',
            cacheEntryTime: cacheObject.lastUsed,
            cacheHeaders: {},
          );

          // Track completion for cache hit
          final responseHeaders = <String, String>{};
          String? contentType;
          cacheResponse.headers.forEach((name, values) {
            final headerValue = values.join(', ');
            responseHeaders[name] = headerValue;
            if (name.toLowerCase() == 'content-type') {
              contentType = headerValue;
            }
          });
          dumper?.recordNetworkRequestComplete(_uri.toString(),
            statusCode: cacheResponse.statusCode,
            responseHeaders: responseHeaders,
            contentType: contentType,
          );

          return cacheResponse;
        }
      }

      // Step 3: Handle negotiate cache request header.
      if (cacheObject.valid &&
          headers.ifModifiedSince == null &&
          headers.value(HttpHeaders.ifNoneMatchHeader) == null) {
        // ETag has higher priority of lastModified.
        if (cacheObject.eTag != null) {
          headers.set(HttpHeaders.ifNoneMatchHeader, cacheObject.eTag!);
        } else if (cacheObject.lastModified != null) {
          headers.set(HttpHeaders.ifModifiedSinceHeader, HttpDate.format(cacheObject.lastModified!));
        }
      }
    }

    request = await _createBackendClientRequest();
    // Send the real data to backend client.
    if (_data.isNotEmpty) {
      await request.addStream(Stream.value(_data));
      _data.clear();
    }

    // Step 4: Send network request
    late HttpClientResponse response;
    bool hitNegotiateCache = false;

    // If cache only, but no cache hit (we'd have returned earlier), abort.
    if (HttpCacheController.mode == HttpCacheMode.CACHE_ONLY) {
      final errorMsg = 'CACHE_ONLY mode but no cache hit';
      // Check if this is a Fetch/XHR request by looking for the marker header
      final isFetchRequest = headers.value('X-WebF-Request-Type') == 'fetch';
      dumper?.recordNetworkRequestError(_uri.toString(), errorMsg, isXHR: isFetchRequest);
      throw FlutterError('HttpCacheMode is CACHE_ONLY, but no cache hit for $uri');
    }

    // Handle response and 304 negotiation
    HttpClientResponse rawResponse;
    try {
      rawResponse = await request.close();
      // Track redirects if any occurred
      if (rawResponse.redirects.isNotEmpty && dumper != null && ownerBundle == null) {
        for (final redirect in rawResponse.redirects) {
          dumper.recordNetworkRequestRedirect(
            _uri.toString(),
            redirect.location.toString(),
            statusCode: redirect.statusCode,
          );
        }
      }
    } catch (e) {
      // If still failing, log and rethrow
      networkLogger.warning('Error closing HTTP request for $uri', e);
      // Check if this is a Fetch/XHR request by looking for the marker header
      final isFetchRequest = headers.value('X-WebF-Request-Type') == 'fetch';
      dumper?.recordNetworkRequestError(_uri.toString(), e.toString(), isXHR: isFetchRequest);
      rethrow;
    }
    response = cacheObject == null
        ? rawResponse
        : await HttpCacheController.instance(origin)
            .interceptResponse(request, rawResponse, cacheObject, _nativeHttpClient, ownerBundle);
    hitNegotiateCache = rawResponse != response;

    // Step 5: Save cookies from response.
    await CookieManager.saveFromResponseRaw(uri, response.headers[HttpHeaders.setCookieHeader]);

    // Track 304 Not Modified response
    if (hitNegotiateCache && response.statusCode == HttpStatus.notModified) {
      dumper?.recordNetworkRequestCacheInfo(_uri.toString(),
        cacheHit: true,
        cacheType: 'network_validated',
        cacheHeaders: {},
      );
    }

    // Track redirects from the final response if any occurred
    if (response.redirects.isNotEmpty && dumper != null && ownerBundle == null) {
      for (final redirect in response.redirects) {
        dumper.recordNetworkRequestRedirect(
          _uri.toString(),
          redirect.location.toString(),
          statusCode: redirect.statusCode,
        );
      }
    }

    // Track final response
    if (dumper != null && ownerBundle == null) {
      final responseHeaders = <String, String>{};
      String? contentType;
      response.headers.forEach((name, values) {
        final headerValue = values.join(', ');
        responseHeaders[name] = headerValue;
        if (name.toLowerCase() == 'content-type') {
          contentType = headerValue;
        }
      });
      dumper.recordNetworkRequestComplete(_uri.toString(),
        statusCode: response.statusCode,
        responseHeaders: responseHeaders,
        contentType: contentType,
      );
    }

    // Check match cache, and then return cache.
    if (hitNegotiateCache) {
      return Future.value(response);
    }

    if (cacheObject != null) {
      // Step 6: Intercept response by cache controller (handle 304).
      // Note: No need to negotiate cache here, this is final response, hit or not hit.
      return HttpCacheController.instance(origin)
          .interceptResponse(request, response, cacheObject, _nativeHttpClient, ownerBundle);
    } else {
      return response;
    }
  } else {
    request = await _createBackendClientRequest();
    // Not using request.add, because large data will cause core exception.
    if (_data.isNotEmpty) {
      await request.addStream(Stream.value(_data));
      _data.clear();
    }
  }

  return await request.close();
}