Пример #1
0
    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);
 }
Пример #4
0
 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);
    }
Пример #8
0
        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));
        }
Пример #9
0
    /// <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));
    }
Пример #10
0
        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);
    }
Пример #12
0
 public async Task MissingContentTypeNotFound()
 {
     await GetApiInfoInvalidResponse(
         _ => new HttpResponseMessage(HttpStatusCode.NotFound),
         ServiceErrors.NotFound, HttpServiceErrors.CreateErrorForStatusCode(HttpStatusCode.NotFound).Message);
 }
Пример #13
0
 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);
                    }
                }
            }
        }