private async Task HandleRequestFailureAsync(HttpContext context, StreamCopyHttpContent requestContent, Exception requestException) { // Check for request body errors, these may have triggered the response error. if (requestContent?.ConsumptionTask.IsCompleted == true) { var(requestBodyCopyResult, requestBodyException) = await requestContent.ConsumptionTask; if (requestBodyCopyResult != StreamCopyResult.Success) { HandleRequestBodyFailure(context, requestBodyCopyResult, requestBodyException, requestException); return; } } // We couldn't communicate with the destination. ReportProxyError(context, ProxyError.Request, requestException); context.Response.StatusCode = StatusCodes.Status502BadGateway; }
private async Task HandleResponseBodyErrorAsync(HttpContext context, StreamCopyHttpContent requestContent, StreamCopyResult responseBodyCopyResult, Exception responseBodyException) { if (requestContent?.ConsumptionTask.IsCompleted == true) { var(requestBodyCopyResult, requestBodyError) = await requestContent.ConsumptionTask; // Check for request body errors, these may have triggered the response error. if (requestBodyCopyResult != StreamCopyResult.Success) { HandleRequestBodyFailure(context, requestBodyCopyResult, requestBodyError, responseBodyException); return; } } var error = responseBodyCopyResult switch { StreamCopyResult.InputError => ProxyError.ResponseBodyDestination, StreamCopyResult.OutputError => ProxyError.ResponseBodyClient, StreamCopyResult.Canceled => ProxyError.ResponseBodyCanceled, _ => throw new NotImplementedException(responseBodyCopyResult.ToString()), }; ReportProxyError(context, error, responseBodyException); if (!context.Response.HasStarted) { // Nothing has been sent to the client yet, we can still send a good error response. context.Response.Clear(); context.Response.StatusCode = StatusCodes.Status502BadGateway; return; } // The response has already started, we must forcefully terminate it so the client doesn't get the // the mistaken impression that the truncated response is complete. ResetOrAbort(context, isCancelled: responseBodyCopyResult == StreamCopyResult.Canceled); }
private StreamCopyHttpContent SetupRequestBodyCopy(HttpRequest request, bool isStreamingRequest, CancellationToken cancellation) { // If we generate an HttpContent without a Content-Length then for HTTP/1.1 HttpClient will add a Transfer-Encoding: chunked header // even if it's a GET request. Some servers reject requests containing a Transfer-Encoding header if they're not expecting a body. // Try to be as specific as possible about the client's intent to send a body. The one thing we don't want to do is to start // reading the body early because that has side-effects like 100-continue. var hasBody = true; var contentLength = request.Headers.ContentLength; var method = request.Method; #if NET var canHaveBodyFeature = request.HttpContext.Features.Get <IHttpRequestBodyDetectionFeature>(); if (canHaveBodyFeature != null) { // 5.0 servers provide a definitive answer for us. hasBody = canHaveBodyFeature.CanHaveBody; } else #endif // https://tools.ietf.org/html/rfc7230#section-3.3.3 // All HTTP/1.1 requests should have Transfer-Encoding or Content-Length. // Http.Sys/IIS will even add a Transfer-Encoding header to HTTP/2 requests with bodies for back-compat. // HTTP/1.0 Connection: close bodies are only allowed on responses, not requests. // https://tools.ietf.org/html/rfc1945#section-7.2.2 // // Transfer-Encoding overrides Content-Length per spec if (request.Headers.TryGetValue(HeaderNames.TransferEncoding, out var transferEncoding) && transferEncoding.Count == 1 && string.Equals("chunked", transferEncoding.ToString(), StringComparison.OrdinalIgnoreCase)) { hasBody = true; } else if (contentLength.HasValue) { hasBody = contentLength > 0; } // Kestrel HTTP/2: There are no required headers that indicate if there is a request body so we need to sniff other fields. else if (!ProtocolHelper.IsHttp2OrGreater(request.Protocol)) { hasBody = false; } // https://tools.ietf.org/html/rfc7231#section-4.3.1 // A payload within a GET/HEAD/DELETE/CONNECT request message has no defined semantics; sending a payload body on a // GET/HEAD/DELETE/CONNECT request might cause some existing implementations to reject the request. // https://tools.ietf.org/html/rfc7231#section-4.3.8 // A client MUST NOT send a message body in a TRACE request. else if (HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsDelete(method) || HttpMethods.IsConnect(method) || HttpMethods.IsTrace(method)) { hasBody = false; } // else hasBody defaults to true StreamCopyHttpContent requestContent = null; if (hasBody) { if (isStreamingRequest) { DisableMinRequestBodyDataRateAndMaxRequestBodySize(request.HttpContext); } // Note on `autoFlushHttpClientOutgoingStream: isStreamingRequest`: // The.NET Core HttpClient stack keeps its own buffers on top of the underlying outgoing connection socket. // We flush those buffers down to the socket on every write when this is set, // but it does NOT result in calls to flush on the underlying socket. // This is necessary because we proxy http2 transparently, // and we are deliberately unaware of packet structure used e.g. in gRPC duplex channels. // Because the sockets aren't flushed, the perf impact of this choice is expected to be small. // Future: It may be wise to set this to true for *all* http2 incoming requests, // but for now, out of an abundance of caution, we only do it for requests that look like gRPC. requestContent = new StreamCopyHttpContent( source: request.Body, autoFlushHttpClientOutgoingStream: isStreamingRequest, clock: _clock, cancellation: cancellation); } return(requestContent); }