I'm trying to validate the value of Content-Type in POST, PUT and PATCH requests, but the current code is only working when I forget the content-type clause or when I use a content-type like: "Content-Type: Foo".
When I send "Content-Type: text/css" I get this:
500 Internal Server Error
No MediaTypeFormatter is available to read an object of type 'MyClassDto' from content with media type 'text/css'.
This is my code:
public class ContentTypeFilter : IActionFilter
{
private readonly List<MediaTypeHeaderValue> _suport;
/// <summary />
public ContentTypeFilterAttribute()
{
_suport = new List<MediaTypeHeaderValue>();
foreach (var formatter in GlobalConfiguration.Configuration.Formatters.ToArray())
{
_suport.AddRange(formatter.SupportedMediaTypes);
}
}
public bool AllowMultiple { get { return false; } }
public Task<HttpResponseMessage> ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
var metodos = new List<string> { "POST", "PUT", "PATCH" };
if (actionContext.Request.Content != null)
{
if (metodos.Contains(actionContext.Request.Method.Method.ToUpperInvariant()))
{
MediaTypeHeaderValue contentType = actionContext.Request.Content.Headers.ContentType;
if (contentType == null || !_suport.Any(x => x.MediaType.Equals(contentType.MediaType)))
{
return CreateResponse(actionContext.Request, "Invalid Content-Type");
}
}
}
return continuation();
}
private static Task<HttpResponseMessage> CreateResponse(HttpRequestMessage request, string mensagem)
{
var tsc = new TaskCompletionSource<HttpResponseMessage>();
var response = request.CreateResponse(HttpStatusCode.UnsupportedMediaType);
response.ReasonPhrase = mensagem;
response.Content = new StringContent(mensagem);
tsc.SetResult(response);
return tsc.Task;
}
Is there another way to validate content-type and return error 415 if the content isn't XML or JSON?
I've found a good solution here.
With some changes to get what I want:
public class ContentTypeFilter : DelegatingHandler
{
private readonly List<MediaTypeHeaderValue> _suport;
/// <summary />
public ContentTypeFilter()
{
_suport = new List<MediaTypeHeaderValue>();
foreach (var formatter in GlobalConfiguration.Configuration.Formatters.ToArray())
{
_suport.AddRange(formatter.SupportedMediaTypes);
}
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var metodos = new List<string> { "POST", "PUT", "PATCH" };
if (request.Content != null)
{
if (metodos.Contains(request.Method.Method.ToUpperInvariant()))
{
MediaTypeHeaderValue contentType = request.Content.Headers.ContentType;
// Nas configurações não possui o Charset aceito.
if (contentType == null || !_suport.Any(x => x.MediaType.Equals(contentType.MediaType)))
{
return Task<HttpResponseMessage>.Factory.StartNew(() => CreateResponse(request, "Suported content-types: " + string.Join(", ", _suport.Select(x => x.ToString()))));
}
}
}
return base.SendAsync(request, cancellationToken);
}
private static HttpResponseMessage CreateResponse(HttpRequestMessage request, string mensagem)
{
var response = request.CreateResponse(HttpStatusCode.UnsupportedMediaType);
response.ReasonPhrase = mensagem;
response.Content = new StringContent(mensagem);
return response;
}
}
Related
I did research and didn't find anything related to that. Please, I hope someone can help me.
What do I have?
I have HttpMessaheHandler that is inherited from DelegatingHandler
What do I do?
I'm trying to handle OAuth2 logic inside that HttpMessaheHandler
So I do another request inside HttpMessaheHandler from freshly created HttpClient
What is the problem?
The problem is that HttpResponseMessage is hit 2 times! It happens on line 70
HttpResponseMessage identityResponse = await this._client.SendAsync(requestMessage);
When this line is executed then we are coming back into HttpResponseMessage and starting this logic from the beginning.
What I have already tried?
I have tried to use ASP.NET Core native functionality services.AddHttpClient(...) the same issue.
What do I expect?
I do expect that my freshly created HttpClient does not hit this HttpMessageHandler as it is not bound to this HttpClient
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Common.Core.Abstractions;
using Common.Data;
using Common.Data.Enums;
using Common.Services.Shared;
using Hrimsoft.StringCases;
using Integrations.ECommerce.Storm.Configuration;
using Integrations.ECommerce.Storm.Models;
using Integrations.ECommerce.Storm.Models.Requests;
using Integrations.ECommerce.Storm.Services;
using Microsoft.Extensions.DependencyInjection;
using UrlCombineLib;
namespace Integrations.ECommerce.Storm
{
internal class StormIdentityHttpMessageHandler : DelegatingHandler
{
private readonly RequestOptions _requestOptions;
private readonly IContextProvider _contextProvider;
private readonly IStormIdentityService _stormIdentityService;
private readonly StormSettings _settings;
private readonly IServiceProvider _serviceProvider;
private readonly string _authorizationHeader = "Authorization";
private readonly string _applicationId = "ApplicationId";
private HttpClient _client;
private IJsonSerializer _jsonSerializer;
public StormIdentityHttpMessageHandler(
IContextProvider contextProvider,
IStormIdentityService stormIdentityService,
StormSettings settings,
IServiceProvider serviceProvider)
{
this._contextProvider = contextProvider;
this._stormIdentityService = stormIdentityService;
this._settings = settings;
this._serviceProvider = serviceProvider;
this._requestOptions = new RequestOptions { ContentType = RequestContentType.FormUrlEncoded, PropertyCaseType = PropertyNameCaseType.SnakeCase };
this._client = new HttpClient { BaseAddress = new Uri(this._settings.IdentityServerUrl) };
this._jsonSerializer = serviceProvider.GetRequiredService<IJsonSerializer>();
}
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
string culture = this._contextProvider.GetCulture();
var parameters = new StormOAuth2AuthenticationParameters
{
GrantType = this._settings.GrantType, ClientId = this._settings.ClientId, ClientSecret = this._settings.ClientSecret, Scope = this._settings.Scope
};
HttpRequestMessage requestMessage = this.Post(string.Empty, parameters, SetRequestOptions);
HttpResponseMessage identityResponse = await this._client.SendAsync(requestMessage);
StormAccessToken token = await RetrieveAccessToken(identityResponse);
// // Handle OAuth2. Requesting/Getting the access token.
// IStormIdentityService identityService = this._stormIdentityService;
// string accessToken = await identityService.RequestAccessToken();
//
// // Setting the AccessToken token to the outgoing request
// request.Headers.Add(this._applicationId, this._settings[culture].ApplicationId);
// request.Headers.Add(this._authorizationHeader, $"Bearer {accessToken}");
// HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
//
// if (response.StatusCode == HttpStatusCode.Unauthorized)
// {
// bool forceNewToken = true;
// accessToken = await identityService.RequestAccessToken(forceNewToken);
// request.Headers.Remove(this._authorizationHeader);
// request.Headers.Add(this._authorizationHeader, $"Bearer {accessToken}");
// response = await base.SendAsync(request, cancellationToken);
//
// if (response.StatusCode == HttpStatusCode.Unauthorized)
// {
// throw new ServiceException($"Can't authorize with current token. Token value:\n{accessToken}");
// }
// }
return response;
}
private async Task<StormAccessToken> RetrieveAccessToken(HttpResponseMessage httpResponse)
{
if (httpResponse.Content == null)
{
return default;
}
string content = await httpResponse.Content.ReadAsStringAsync();
if (string.IsNullOrEmpty(content))
{
return default;
}
return JsonSerializer.Deserialize<StormAccessToken>(content);
}
protected virtual async Task<Exception> OnNegativeResponse(HttpResponseMessage response)
{
HttpRequestMessage request = response.RequestMessage;
string details = null;
if (response.Content != null)
{
details = await response.Content.ReadAsStringAsync();
}
string message = $"REST request '{request.Method} - {request.RequestUri}' " +
$"error: {(int)response.StatusCode}, Message: {response.ReasonPhrase}{Environment.NewLine}{details}";
var exception = new Exception(message);
return exception;
}
protected HttpRequestMessage Post(string url = "", object data = null, Action<RequestOptions> setup = null)
=> BuildRequest(HttpMethod.Post, url, data, setup);
protected virtual string BuildResourceUrl(string baseUrl, string resource)
=> UrlCombine.Combine(baseUrl, resource);
private HttpRequestMessage BuildRequest(
HttpMethod method,
string resourceBase,
object body = null,
Action<RequestOptions> setup = null)
{
string fullUrl = BuildResourceUrl(this._client.BaseAddress.ToString(), resourceBase);
var request = new HttpRequestMessage(method, fullUrl);
if (body == null)
{
return request;
}
var options = new RequestOptions();
setup?.Invoke(options);
if (options.ContentType == RequestContentType.FormUrlEncoded)
{
Dictionary<string, string> content = GetFormContent(body, options);
request.Content = new FormUrlEncodedContent(content);
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
}
else
{
throw new Exception($"Unsupported content type {options.ContentType}");
}
return request;
}
private Dictionary<string, string> GetFormContent(object data, RequestOptions options)
{
var pairs = data
.GetType()
.GetProperties()
.Select(s => new { Name = ToRequiredCase(s.Name, options.PropertyCaseType), Value = s.GetValue(data, null)?.ToString() })
.Where(s => s.Value != null)
.ToDictionary(k => k.Name, v => v.Value);
return pairs;
string ToRequiredCase(string value, PropertyNameCaseType type)
{
string processedValue = string.Empty;
if (type == PropertyNameCaseType.CamelCase)
{
processedValue = value?.ToCamelCase();
}
else if (type == PropertyNameCaseType.SnakeCase)
{
processedValue = value?.ToSnakeCase();
}
return processedValue;
}
}
private void SetRequestOptions(RequestOptions options)
{
options.ContentType = this._requestOptions.ContentType;
options.PropertyCaseType = this._requestOptions.PropertyCaseType;
}
}
}
I am exploring how IIS/Owin pipeline works. I am trying to find the library/method used by IIS/Owin down the pipeline to convert IHttpActionResult (returned from controller) into the correct content-type like application/json as present in the request.
Controller -
[Route("")]
public IHttpActionResult Get()
{
IEnumerable<Product> productList = ProductService.GetAllProducts();
if (!productList.Any())
return Ok();
return Json(productList, new JsonSerializerSettings
{
ContractResolver = new WebContractResolver(),
Converters = new List<JsonConverter> { new TrimStringDataConverter() }
});
}
Data received by API consumer -
[
{
"code": "prod101",
"title": "LAPTOP"
},
{
"code": "prod102",
"title": "MOBILE"
}
]
When this conversion from IHttpActionResult to application/json takes place ?
IHttpActionResult has a method ExecuteAsync which returns HttpResponseMessage
public interface IHttpActionResult
{
Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken);
}
When you use Json() in your controller it will create a new JsonResult(link)
And this is the method which create a HttpResponseMessage which is a json.(link)
public virtual Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
try
{
ArraySegment<byte> segment = Serialize();
response.Content = new ByteArrayContent(segment.Array, segment.Offset, segment.Count);
MediaTypeHeaderValue contentType = new MediaTypeHeaderValue("application/json");
contentType.CharSet = _encoding.WebName;
response.Content.Headers.ContentType = contentType;
response.RequestMessage = _dependencies.Request;
}
catch
{
response.Dispose();
throw;
}
return response;
}
in Owin there is a HttpMessageHandlerAdapter class which has a SendResponseMessageAsync method that return HttpResponseMessage to client. Here's the source from github: HttpMessageHandlerAdapter
private Task SendResponseMessageAsync(HttpRequestMessage request, HttpResponseMessage response,
IOwinResponse owinResponse, CancellationToken cancellationToken)
{
owinResponse.StatusCode = (int)response.StatusCode;
owinResponse.ReasonPhrase = response.ReasonPhrase;
// Copy non-content headers
IDictionary<string, string[]> responseHeaders = owinResponse.Headers;
foreach (KeyValuePair<string, IEnumerable<string>> header in response.Headers)
{
responseHeaders[header.Key] = header.Value.AsArray();
}
HttpContent responseContent = response.Content;
if (responseContent == null)
{
SetHeadersForEmptyResponse(responseHeaders);
return TaskHelpers.Completed();
}
else
{
// Copy content headers
foreach (KeyValuePair<string, IEnumerable<string>> contentHeader in responseContent.Headers)
{
responseHeaders[contentHeader.Key] = contentHeader.Value.AsArray();
}
// Copy body
return SendResponseContentAsync(request, response, owinResponse, cancellationToken);
}
}
I'm trying to pull an HttpError out of an HttpResponseMessage message which may or not be there. If the Api throws an exception it will be serialised as an HttpError however errors such as 404's will not be in this format.
I've managed to fix this bug in the code below by catching to exception thrown if we fail to deserialize the HttpError.
The issue is now I'm using exception driven development.
Idealy I want something like this.
var httpError = await response.Content.TryReadAsAsync<HttpError>(formatters);
if (httpError == null)
{
// Definetly not an HttpError and no exception thrown
}
Surely the must be an easy way of telling the type of the content in the HttpContent?
public static async Task<ApiResponseMessage<T>> GetApiResponseAsync<T>(this HttpResponseMessage response, IEnumerable<MediaTypeFormatter> formatters) where T : class
{
if (!response.IsSuccessStatusCode)
{
HttpError httpError;
// Exception driven programming
try
{
// Could use string?
var contentString = response.Content.ReadAsStringAsync();
// This doesn't work. Throws exception if not correct type
var contentObject = await response.Content.ReadAsAsync<object>();
var alwaysNull = contentObject as HttpError;
httpError = await response.Content.ReadAsAsync<HttpError>(formatters);
}
catch (Exception)
{
httpError = null;
}
return new ApiResponseMessage<T>
{
IsSuccess = false,
HttpError = httpError,
Response = response
};
}
return new ApiResponseMessage<T>
{
IsSuccess = true,
Result = await response.Content.ReadAsAsync<T>(formatters),
Response = response
};
}
Cleaned up the code so it at least compiles.
public class ReadAsyncResult<T>
{
public ReadAsyncResult()
{
}
public ReadAsyncResult(T result)
{
Result = result;
IsSuccess = result != null;
}
public T Result { get; set; }
public bool IsSuccess { get; set; }
public static async Task<ReadAsyncResult<T>> TryReadAsAsync<T>(HttpContent content)
{
return await TryReadAsAsync<T>(content, CancellationToken.None);
}
public static async Task<ReadAsyncResult<T>> TryReadAsAsync<T>(HttpContent content,
CancellationToken cancellationToken)
{
if (content == null)
return new ReadAsyncResult<T>();
var type = typeof(T);
var objectContent = content as ObjectContent;
if (objectContent?.Value != null && type.IsInstanceOfType(objectContent.Value))
{
return new ReadAsyncResult<T>((T) objectContent.Value);
}
var mediaType = content.Headers.ContentType;
var reader =
new MediaTypeFormatterCollection(new MediaTypeFormatterCollection()).FindReader(type, mediaType);
if (reader == null) return new ReadAsyncResult<T>();
var value = await ReadAsAsyncCore<T>(content, type, reader, cancellationToken);
return new ReadAsyncResult<T>(value);
}
private static async Task<T> ReadAsAsyncCore<T>(HttpContent content, Type type, MediaTypeFormatter formatter,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var stream = await content.ReadAsStreamAsync();
var result = await formatter.ReadFromStreamAsync(type, stream, content, null, cancellationToken);
return (T) result;
}
}
It is of course, blindingly simple.
var message = new HttpResponseMessage();
HttpError httpError;
message.TryGetContentValue(out httpError);
if (httpError != null)
{
// Do stuff
}
Edit:
This didn't fix my issue as the content type was not of type ObjectResult. I was expecting the TryGetContentValue to work in the same way as HttpContent.ReadAsAsync.
After digging through the source code for ReadAsAsync I have created a working solution.
public class ReadAsyncResult<T>
{
public ReadAsyncResult()
{
}
public ReadAsyncResult(T result)
{
Result = result;
IsSuccess = result != null;
}
public T Result { get; set; }
public bool IsSuccess { get; set; }
}
public static async Task<ReadAsyncResult<T>> TryReadAsAsync<T>(this HttpContent content)
{
return await TryReadAsAsync<T>(content, CancellationToken.None);
}
public static async Task<ReadAsyncResult<T>> TryReadAsAsync<T>(this HttpContent content, CancellationToken cancellationToken)
{
if (content == null)
return new ReadAsyncResult<T>();
var type = typeof(T);
var objectContent = content as ObjectContent;
if (objectContent?.Value != null && type.IsInstanceOfType(objectContent.Value))
{
return new ReadAsyncResult<T>((T)objectContent.Value);
}
var mediaType = content.Headers.ContentType;
var reader = new MediaTypeFormatterCollection(new MediaTypeFormatterCollection()).FindReader(type, mediaType);
if (reader == null) return new ReadAsyncResult<T>();
var value = await ReadAsAsyncCore<T>(content, type, reader, cancellationToken);
return new ReadAsyncResult<T>(value);
}
private static async Task<T> ReadAsAsyncCore<T>(HttpContent content, Type type, MediaTypeFormatter formatter, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var stream = await content.ReadAsStreamAsync();
var result = await formatter.ReadFromStreamAsync(type, stream, content, null, cancellationToken);
return (T) result;
}
I'm using this code to return an object content, but I would like to cache the response adding the Cache-Control headers.
[AllowAnonymous]
[Route("GetPublicContent")]
[HttpGet]
public IHttpActionResult GetPublicContent([FromUri]UpdateContentDto dto)
{
if (dto == null)
return BadRequest();
var content = _contentService.GetPublicContent(dto);
if (content == null)
return BadRequest();
return new Ok(content);
}
Just that! Thanks!!
Make a new class that inherits from OkNegotiatedContentResult<T>:
public class CachedOkResult<T> : OkNegotiatedContentResult<T>
{
public CachedOkResult(T content, TimeSpan howLong, ApiController controller) : base(content, controller)
{
HowLong = howLong;
}
public CachedOkResult(T content, IContentNegotiator contentNegotiator, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters)
: base(content, contentNegotiator, request, formatters) { }
public TimeSpan HowLong { get; private set; }
public override async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
var response = await base.ExecuteAsync(cancellationToken);
response.Headers.CacheControl = new CacheControlHeaderValue() {
Public = false,
MaxAge = HowLong
};
return response;
}
}
To use this in your controller, just return a new instance of the CachedOkResult class:
public async Task<IHttpActionResult> GetSomething(string id)
{
var value = await GetAsyncResult(id);
// cache result for 60 seconds
return new CachedOkResult<string>(value, TimeSpan.FromSeconds(60), this);
}
The headers will come across the wire like this:
Cache-Control:max-age=60
Content-Length:551
Content-Type:application/json; charset=utf-8
... other headers snipped ...
You can set it like this
public HttpResponseMessage GetFoo(int id) {
var foo = _FooRepository.GetFoo(id);
var response = Request.CreateResponse(HttpStatusCode.OK, foo);
response.Headers.CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = new TimeSpan(1, 0, 0, 0)
};
return response;
}
Update
Or try this question
From my MVC3 controller action I want to return HTTP 403, set "status description" to some specific string and also return that string in the result content so that it is visible in the browser.
I can return ContentResult to specify content, but not a status code (such as 403) and not a status description. I can use HttpStatusCodeResult to specify a status code and status description but not the result content.
How do I craft an action result that contains all three?
Commonly you would see this done by setting the response code then returning a regular ActionResult
public ActionResult Foo()
{
Response.StatusCode = 403;
Response.StatusDescription = "Some custom message";
return View(); // or Content(), Json(), etc
}
If you really need this to be an ActionResult, you create your own.
Example:
public class HttpStatusContentResult : ActionResult
{
private string _content;
private HttpStatusCode _statusCode;
private string _statusDescription;
public HttpStatusContentResult(string content,
HttpStatusCode statusCode = HttpStatusCode.OK,
string statusDescription = null)
{
_content = content;
_statusCode = statusCode;
_statusDescription = statusDescription;
}
public override void ExecuteResult(ControllerContext context)
{
var response = context.HttpContext.Response;
response.StatusCode = (int) _statusCode;
if (_statusDescription != null)
{
response.StatusDescription = _statusDescription;
}
if (_content != null)
{
context.HttpContext.Response.Write(_content);
}
}
}
If this is not too dirty
Response.Clear();
Response.Write("Some specific string");
return new HttpStatusCodeResult(403, "another specific string");
I went crazy trying to get this code to work before I realized it was the GetAwaiter().OnCompleted(...) that was the problem. Here's the version I got working:
public class ApiControllerBase : ApiController
{
...
// Other code
...
public override Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
{
return base
.ExecuteAsync(controllerContext, cancellationToken)
.ContinueWith(t =>
{
t.Result.Headers.CacheControl = new CacheControlHeaderValue()
{
NoStore = true,
NoCache = true,
MaxAge = new TimeSpan(0),
MustRevalidate = true
};
t.Result.Headers.Pragma.Add(new NameValueHeaderValue("no-cache"));
t.Result.Content.Headers.Expires = DateTime.Parse("01 Jan 1990 00:00:00 GMT");
return t.Result;
}, cancellationToken);
}
}