protected override async Task <ServiceResult <object> > ReadHttpContentAsyncCore(Type objectType, HttpContent content, CancellationToken cancellationToken) { try { #if NET6_0_OR_GREATER var stream = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var streamScope = stream.ConfigureAwait(false); #elif NETSTANDARD2_1_OR_GREATER var stream = await content.ReadAsStreamAsync().ConfigureAwait(false); await using var streamScope = stream.ConfigureAwait(false); #else using var stream = await content.ReadAsStreamAsync().ConfigureAwait(false); #endif var deserializedContent = await m_serializer.FromStreamAsync(stream, objectType, cancellationToken).ConfigureAwait(false); if (deserializedContent is null) { return(ServiceResult.Failure(HttpServiceErrors.CreateInvalidContent("Content must not be empty."))); } return(ServiceResult.Success(deserializedContent)); } catch (ServiceSerializationException exception) { return(ServiceResult.Failure(HttpServiceErrors.CreateInvalidContent(exception.Message))); } }
public async Task UnsupportedContentTypeNotFound() { await GetApiInfoInvalidResponse( _ => new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent("text", Encoding.UTF8, "text/plain") }, ServiceErrors.NotFound, HttpServiceErrors.CreateErrorForStatusCode(HttpStatusCode.NotFound).Message); }
public async Task InvalidJsonNotFound() { await GetApiInfoInvalidResponse( _ => new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent("{", Encoding.UTF8, "application/json") }, ServiceErrors.NotFound, HttpServiceErrors.CreateErrorForStatusCode(HttpStatusCode.NotFound).Message); }
private static IActionResult CreateActionResultFromError(ServiceErrorDto error) { return(new ContentResult { Content = ServiceJsonUtility.ToJson(error), ContentType = HttpServiceUtility.JsonMediaType, StatusCode = (int)(HttpServiceErrors.TryGetHttpStatusCode(error.Code) ?? HttpStatusCode.InternalServerError), }); }
public async Task EmptyJson() { await GetApiInfoInvalidResponse( _ => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("", Encoding.UTF8, "application/json") }, ServiceErrors.InvalidResponse, HttpServiceErrors.CreateInvalidContent("").Message); }
public static HttpResponseMessage CreateHttpResponseMessage(Exception exception) { var error = ServiceErrorUtility.CreateInternalErrorForException(exception); var statusCode = HttpServiceErrors.TryGetHttpStatusCode(error.Code) ?? HttpStatusCode.InternalServerError; return(new HttpResponseMessage(statusCode) { Content = JsonHttpContentSerializer.Instance.CreateHttpContent(error), }); }
/// <summary> /// Called to create an error object from an unhandled HTTP response. /// </summary> protected virtual async Task <ServiceErrorDto> CreateErrorFromHttpResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken) { var result = await ContentSerializer.ReadHttpContentAsync <ServiceErrorDto>(response.Content, cancellationToken).ConfigureAwait(false); if (result.IsFailure || string.IsNullOrWhiteSpace(result.Value.Code)) { return(HttpServiceErrors.CreateErrorForStatusCode(response.StatusCode, response.ReasonPhrase)); } return(result.Value); }
public async Task NotFoundHtmlClientError() { var service = CreateTestService(request => { var response = new HttpResponseMessage(HttpStatusCode.NotFound); response.Content = new StringContent("<html><body><h1>Not Found!</h1></body></html>"); response.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("text/html"); return(response); }); var result = await service.CreateWidgetAsync(new WidgetDto(name : "hi")); result.Error.Should().BeDto(HttpServiceErrors.CreateErrorForStatusCode(HttpStatusCode.NotFound)); }
/// <summary> /// Reads an object from the specified HTTP content. /// </summary> public async Task <ServiceResult <object> > ReadHttpContentAsync(Type objectType, HttpContent?content, CancellationToken cancellationToken = default) { var contentType = content?.Headers.ContentType; if (contentType == null) { return(ServiceResult.Failure(HttpServiceErrors.CreateMissingContentType())); } var mediaType = contentType.MediaType ?? ""; if (!IsSupportedMediaType(mediaType)) { return(ServiceResult.Failure(HttpServiceErrors.CreateUnsupportedContentType(mediaType))); } return(await ReadHttpContentAsyncCore(objectType, content !, cancellationToken).ConfigureAwait(false)); }
public async Task BadIfNoneMatch() { var service = TestUtility.CreateService(m_category); var widget = InMemoryExampleApiRepository.SampleWidgets[0]; string eTag = ExampleApiService.CreateWidgetETag(widget); var result = await service.GetWidgetAsync(new GetWidgetRequestDto { Id = widget.Id, IfNoneMatch = "xyzzy" }, CancellationToken.None); if (m_category == "InMemory") { result.Should().BeSuccess(new GetWidgetResponseDto { Widget = widget, ETag = eTag }); } else { result.Should().BeFailure(HttpServiceErrors.CreateHeaderInvalidFormat("If-None-Match")); } }
/// <summary> /// Attempts to handle a service method. /// </summary> protected async Task <HttpResponseMessage?> TryHandleServiceMethodAsync <TRequest, TResponse>(HttpMethodMapping <TRequest, TResponse> mapping, HttpRequestMessage httpRequest, Func <TRequest, CancellationToken, Task <ServiceResult <TResponse> > > invokeMethodAsync, CancellationToken cancellationToken) where TRequest : ServiceDto, new() where TResponse : ServiceDto, new() { if (mapping == null) { throw new ArgumentNullException(nameof(mapping)); } if (httpRequest == null) { throw new ArgumentNullException(nameof(httpRequest)); } if (invokeMethodAsync == null) { throw new ArgumentNullException(nameof(invokeMethodAsync)); } if (httpRequest.RequestUri == null) { throw new ArgumentException("RequestUri must be specified.", nameof(httpRequest)); } if (httpRequest.Method != mapping.HttpMethod) { return(null); } var pathParameters = TryMatchHttpRoute(httpRequest.RequestUri, m_rootPath + mapping.Path); if (pathParameters == null) { return(null); } var context = new ServiceHttpContext(); ServiceHttpContext.SetContext(httpRequest, context); var aspectHttpResponse = await AdaptTask(RequestReceivedAsync(httpRequest, cancellationToken)).ConfigureAwait(true); if (aspectHttpResponse != null) { return(aspectHttpResponse); } ServiceErrorDto?error = null; object?requestBody = null; if (mapping.RequestBodyType != null) { try { var serializer = GetHttpContentSerializer(mapping.RequestBodyType); var requestResult = await AdaptTask(serializer.ReadHttpContentAsync(mapping.RequestBodyType, httpRequest.Content, cancellationToken)).ConfigureAwait(true); if (requestResult.IsFailure) { error = requestResult.Error; } else { requestBody = requestResult.Value; } } catch (Exception exception) when(ShouldCreateErrorFromException(exception)) { // cancellation can cause the wrong exception cancellationToken.ThrowIfCancellationRequested(); // error reading request body error = CreateErrorFromException(exception); } } TResponse?response = null; if (error == null) { var request = mapping.CreateRequest(requestBody); var uriParameters = new Dictionary <string, string?>(StringComparer.OrdinalIgnoreCase); foreach (var queryParameter in ParseQueryString(httpRequest.RequestUri.Query)) { uriParameters[queryParameter.Key] = queryParameter.Value[0]; } foreach (var pathParameter in pathParameters) { uriParameters[pathParameter.Key] = pathParameter.Value; } request = mapping.SetUriParameters(request, uriParameters); request = mapping.SetRequestHeaders(request, HttpServiceUtility.CreateDictionaryFromHeaders(httpRequest.Headers, httpRequest.Content?.Headers) !); context.Request = request; if (!m_skipRequestValidation && !request.Validate(out var requestErrorMessage)) { error = ServiceErrors.CreateInvalidRequest(requestErrorMessage); } else { var methodResult = await invokeMethodAsync(request, cancellationToken).ConfigureAwait(true); if (methodResult.IsFailure) { error = methodResult.Error; } else { response = methodResult.Value; if (!m_skipResponseValidation && !response.Validate(out var responseErrorMessage)) { error = ServiceErrors.CreateInvalidResponse(responseErrorMessage); response = null; } } } context.Result = error != null?ServiceResult.Failure(error) : ServiceResult.Success <ServiceDto>(response !); } HttpResponseMessage httpResponse; if (error == null) { var responseMappingGroups = mapping.ResponseMappings .GroupBy(x => x.MatchesResponse(response !)) .Where(x => x.Key != false) .OrderByDescending(x => x.Key) .ToList(); if (responseMappingGroups.Count >= 1 && responseMappingGroups[0].Count() == 1) { var responseMapping = responseMappingGroups[0].Single(); httpResponse = new HttpResponseMessage(responseMapping.StatusCode); var responseHeaders = mapping.GetResponseHeaders(response !); var headersResult = HttpServiceUtility.TryAddNonContentHeaders(httpResponse.Headers, responseHeaders); if (headersResult.IsFailure) { throw new InvalidOperationException(headersResult.Error !.Message); } if (responseMapping.ResponseBodyType != null) { var serializer = GetHttpContentSerializer(responseMapping.ResponseBodyType); var mediaType = responseMapping.ResponseBodyContentType ?? responseHeaders?.GetContentType() ?? GetAcceptedMediaType(httpRequest, serializer); httpResponse.Content = serializer.CreateHttpContent(responseMapping.GetResponseBody(response !) !, mediaType); if (m_disableChunkedTransfer) { await httpResponse.Content.LoadIntoBufferAsync().ConfigureAwait(false); } } } else { throw new InvalidOperationException($"Found {responseMappingGroups.Sum(x => x.Count())} valid HTTP responses for {typeof(TResponse).Name}: {response}"); } } else { var statusCode = error.Code == null ? HttpStatusCode.InternalServerError : (TryGetCustomHttpStatusCode(error.Code) ?? HttpServiceErrors.TryGetHttpStatusCode(error.Code) ?? HttpStatusCode.InternalServerError); httpResponse = new HttpResponseMessage(statusCode); if (statusCode != HttpStatusCode.NoContent && statusCode != HttpStatusCode.NotModified) { var mediaType = GetAcceptedMediaType(httpRequest, m_contentSerializer); httpResponse.Content = m_contentSerializer.CreateHttpContent(error, mediaType); if (m_disableChunkedTransfer) { await httpResponse.Content.LoadIntoBufferAsync().ConfigureAwait(false); } } } httpResponse.RequestMessage = httpRequest; await AdaptTask(ResponseReadyAsync(httpResponse, cancellationToken)).ConfigureAwait(true); return(httpResponse); }
public async Task MissingContentTypeNotFound() { await GetApiInfoInvalidResponse( _ => new HttpResponseMessage(HttpStatusCode.NotFound), ServiceErrors.NotFound, HttpServiceErrors.CreateErrorForStatusCode(HttpStatusCode.NotFound).Message); }
public async Task MissingContentType() { await GetApiInfoInvalidResponse( _ => new HttpResponseMessage(HttpStatusCode.OK), ServiceErrors.InvalidResponse, HttpServiceErrors.CreateMissingContentType().Message); }
/// <summary> /// Invokes the middleware. /// </summary> /// <remarks>This method translates the ASP.NET Core request into an HttpRequestMessage, /// uses the ServiceHttpHandler to handle the message, and then converts the HttpResponseMessage /// into an ASP.NET Core response. We borrowed code from Microsoft.AspNetCore.Mvc.WebApiCompatShim /// (HttpRequestMessageFeature and HttpResponseMessageOutputFormatter) to make sure we got it right /// and to avoid requiring an dependency on ASP.NET Core MVC and WebApiCompatShim.</remarks> public async Task Invoke(HttpContext httpContext) { var httpRequest = httpContext.Request; var uriString = httpRequest.Scheme + "://" + httpRequest.Host + httpRequest.PathBase + httpRequest.Path + httpRequest.QueryString; var requestMessage = new HttpRequestMessage(new HttpMethod(httpRequest.Method), uriString); // This allows us to pass the message through APIs defined in legacy code and then // operate on the HttpContext inside. requestMessage.Properties[nameof(HttpContext)] = httpContext; requestMessage.Content = new StreamContent(httpRequest.Body); foreach (var header in httpRequest.Headers) { // Every header should be able to fit into one of the two header collections. // Try message.Headers first since that accepts more of them. if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable())) { requestMessage.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable()); } } HttpResponseMessage responseMessage; try { responseMessage = await m_handler.TryHandleHttpRequestAsync(requestMessage, httpContext.RequestAborted).ConfigureAwait(false); } catch (Exception exception) { var error = ServiceErrorUtility.CreateInternalErrorForException(exception); var statusCode = HttpServiceErrors.TryGetHttpStatusCode(error.Code) ?? HttpStatusCode.InternalServerError; responseMessage = new HttpResponseMessage(statusCode) { Content = JsonHttpContentSerializer.Instance.CreateHttpContent(error) }; } if (responseMessage != null) { using (responseMessage) { var response = httpContext.Response; response.StatusCode = (int)responseMessage.StatusCode; var responseHeaders = responseMessage.Headers; // Ignore the Transfer-Encoding header if it is just "chunked". // We let the host decide about whether the response should be chunked or not. if (responseHeaders.TransferEncodingChunked == true && responseHeaders.TransferEncoding.Count == 1) { responseHeaders.TransferEncoding.Clear(); } foreach (var header in responseHeaders) { response.Headers.Append(header.Key, header.Value.ToArray()); } if (responseMessage.Content != null) { var contentHeaders = responseMessage.Content.Headers; // Copy the response content headers only after ensuring they are complete. // We ask for Content-Length first because HttpContent lazily computes this // and only afterwards writes the value into the content headers. var unused = contentHeaders.ContentLength; foreach (var header in contentHeaders) { response.Headers.Append(header.Key, header.Value.ToArray()); } await responseMessage.Content.CopyToAsync(response.Body).ConfigureAwait(false); } } } else { await m_next(httpContext).ConfigureAwait(false); } }
private async Task HostAsync(HttpContext httpContext) { var httpRequest = httpContext.Request; var requestUrl = httpRequest.GetEncodedUrl(); var apiHandler = new ConformanceApiHttpHandler(new ConformanceApiService(m_tests)); var requestMessage = new HttpRequestMessage(new HttpMethod(httpRequest.Method), requestUrl) { Content = new StreamContent(httpRequest.Body), }; foreach (var header in httpRequest.Headers) { // Every header should be able to fit into one of the two header collections. // Try message.Headers first since that accepts more of them. if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable())) { requestMessage.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable()); } } HttpResponseMessage?responseMessage = null; ServiceErrorDto? error = null; try { responseMessage = await apiHandler.TryHandleHttpRequestAsync(requestMessage, httpContext.RequestAborted).ConfigureAwait(false); if (responseMessage == null) { error = ServiceErrors.CreateInvalidRequest($"Test not found for {httpRequest.Method} {requestUrl}"); } } catch (Exception exception) { error = ServiceErrorUtility.CreateInternalErrorForException(exception); } if (error != null) { var statusCode = HttpServiceErrors.TryGetHttpStatusCode(error.Code) ?? HttpStatusCode.InternalServerError; responseMessage = new HttpResponseMessage(statusCode) { Content = JsonHttpContentSerializer.Instance.CreateHttpContent(error) }; } if (responseMessage != null) { using (responseMessage) { var response = httpContext.Response; response.StatusCode = (int)responseMessage.StatusCode; var responseHeaders = responseMessage.Headers; // Ignore the Transfer-Encoding header if it is just "chunked". // We let the host decide about whether the response should be chunked or not. if (responseHeaders.TransferEncodingChunked == true && responseHeaders.TransferEncoding.Count == 1) { responseHeaders.TransferEncoding.Clear(); } foreach (var header in responseHeaders) { response.Headers.Append(header.Key, header.Value.ToArray()); } // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (responseMessage.Content != null) { var contentHeaders = responseMessage.Content.Headers; // Copy the response content headers only after ensuring they are complete. // We ask for Content-Length first because HttpContent lazily computes this // and only afterwards writes the value into the content headers. _ = contentHeaders.ContentLength; foreach (var header in contentHeaders) { response.Headers.Append(header.Key, header.Value.ToArray()); } await responseMessage.Content.CopyToAsync(response.Body).ConfigureAwait(false); } } } }