I have a simple controller in ASP.NET Core that proxies images from the web and provides a resized version when query parameters are passed.
Some of the source images do not have a Content-Length and Transfer-Encoding: chunked, but...
The base class FileResultExecutorBase does not set the Transfer-Encoding header, even though the ContentLength can be null. My understanding is, that this header is necessary when Content-Length is not provided.
When I set the header manually it is ignored.
My sample code:
class Controller
{
public async Task<IActionResult> Proxy(string url)
{
var response = await httpClient.GetAsync(url);
return new FileCallbackResult("", (stream, context) =>
{
await using (var sourceStream = await response.Content.ReadAsStreamAsync())
{
await sourceStream.CopyToAsync(stream);
}
};
}
}
public delegate Task FileCallback(Stream body, HttpContext httpContext);
public sealed class FileCallbackResult : FileResult
{
public FileCallback Callback { get; }
public FileCallbackResult(string contentType, FileCallback callback)
: base(contentType)
{
Callback = callback;
}
public override Task ExecuteResultAsync(ActionContext context)
{
var executor = context.HttpContext.RequestServices.GetRequiredService<FileCallbackResultExecutor>();
return executor.ExecuteAsync(context, this);
}
}
public sealed class FileCallbackResultExecutor : FileResultExecutorBase
{
public async Task ExecuteAsync(ActionContext context, FileCallbackResult result)
{
var (_, _, serveBody) = SetHeadersAndLog(context, result, result.FileSize, result.FileSize.HasValue);
if (result.FileSize == null)
{
context.HttpContext.Response.Headers[HeaderNames.TransferEncoding] = "chunked";
}
if (serveBody)
{
var bytesRange = new BytesRange(range?.From, range?.To);
await result.Callback(
context.HttpContext.Response.Body,
context.HttpContext);
}
}
}
It works fine locally, but I deploy it to our kubernetes cluster with istio, the requests cannot be served. I guess that istio or another proxy does not like the header combination. I have not found anything in the logs yet.
Related
I wrote a controller. I wrote it according to the web api I will use here. But how should I make my own created api?
Do I need to have my own created api where I write with HttpPost? I might be wrong as I am new to this.
public class GoldPriceDetailController : Controller
{
string Baseurl = "https://apigw.bank.com.tr:8003/";
public async Task<ActionResult> GetGoldPrice()
{
List<GoldPrice> goldPriceList = new List<GoldPrice>();
using (var client = new HttpClient())
{
//Passing service base url
client.BaseAddress = new Uri(Baseurl);
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
//Sending request to find web api REST service resource GetDepartments using HttpClient
HttpResponseMessage Res = await client.GetAsync("getGoldPrices");
Console.WriteLine(Res.Content);
//Checking the response is successful or not which is sent using HttpClient
if (Res.IsSuccessStatusCode)
{
var ObjResponse = Res.Content.ReadAsStringAsync().Result;
goldPriceList = JsonConvert.DeserializeObject<List<GoldPrice>>(ObjResponse);
}
//returning the student list to view
return View(goldPriceList);
}
}
[HttpPost]
public async Task<IActionResult> GetReservation(int id)
{
GoldPrice reservation = new GoldPrice();
using (var httpClient = new HttpClient())
{
using (var response = await httpClient.GetAsync("https://apigw.bank.com.tr:8443/getGoldPrices" + id))
{
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
string apiResponse = await response.Content.ReadAsStringAsync();
reservation = JsonConvert.DeserializeObject<GoldPrice>(apiResponse);
}
else
ViewBag.StatusCode = response.StatusCode;
}
}
return View(reservation);
}
}
Basically you need these steps:
HttpClient for local API.
HttpClient for external API.
Local API controller.
Inject external client into the local API.
Then inject the local API client into the razor page/controller.
HttpClient for Local API
public class LocalApiClient : ILocalHttpClient
{
private readonly HttpClient _client;
public LocalAPiClient(HttpClient client)
{
_client = client;
_client.BaseAddress(new Uri("https://localapi.com"));
}
[HttpGet]
public async Task<string> GetGoldPrices(int id)
{
// logic to get prices from local api
var response = await _client.GetAsync($"GetGoldPrices?id={id}");
// deserialize or other logic
}
}
HttpClient for External API
public class ExternalApiClient : IExternalHttpClient
{
private readonly HttpClient _client;
public ExternalAPiClient(HttpClient client)
{
_client = client;
_client.BaseAddress(new Uri("https://externalApi.com"));
// ...
}
[HttpGet]
public async Task<string> GetGoldPrices(int id)
{
// logic to get prices from external api
var response = await _client.GetAsync("getGoldPrices?id=" + id))
}
}
Register your clients in startup
services.AddHttpClient<ILocalHttpClient, LocalHttpClient>();
services.AddHttpClient<IExternalHttpClient, ExternalHttpClient>();
Create Local API Controller
and inject the external http client into it
[ApiController]
public class LocalAPIController : Controller
{
private readonly IExternalHttpClient _externalClient;
public LocalAPIController(IExternalHttpClient externalClient)
{
_externalClient = externalClient;
}
[HttpGet]
public async Task<string> GetGoldPrices(int id)
{
var resoponse = await _externalClient.GetGoldPrices(id);
// ...
}
}
Inject the local client into razor page/controller
public class HomeController : Controller
{
private readonly ILocalHttpClient _localClient;
public HomeController(ILocalHttpClient localClient)
{
_localClient = localClient;
}
public async Task<IActionResult> Index(int id)
{
var response = await _localClient.GetGoldPrices(id);
// ...
}
}
I want to support all 3 of the following content-types in my controller/action.
application/json
application/x-www-form-urlencoded
multipart/form-data
with this signature i can support both urlencoded and form data, however a JSON payload does not get bound to Message
[HttpPost]
public async Task<IActionResult> PostAsync(Message message)
If i want to bind a JSON payload to Message properly i need to use the FromBody attribute like this:
[HttpPost]
public async Task<IActionResult> PostAsync([FromBody]Message message)
however doing this starts throwing 415 erros for the other 2 content types I'm interested in.
My question is, how can I provide a single API endpoint to my customers and give them the flexibility of sending data in any of these 3 content types.
You should add the different content types supported using ConsumesAttribute then update your action as follows;
[HttpPost]
[Consumes("application/json", "multipart/form-data", "application/x-www-form-urlencoded")]
public async Task<IActionResult> PostAsync([FromForm, FromBody, FromQuery]Message message)
First, you should avoid combining the application/json and multipart/form-data for the same action which will make application unstable.
If you insist on this, you need to implement your own ModelBinder by following steps below:
MyComplexTypeModelBinder
public class MyComplexTypeModelBinder : ComplexTypeModelBinder
{
public MyComplexTypeModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders, ILoggerFactory loggerFactory, bool allowValidatingTopLevelNodes) : base(propertyBinders, loggerFactory, allowValidatingTopLevelNodes)
{
}
protected override Task BindProperty(ModelBindingContext bindingContext)
{
try
{
var result = base.BindProperty(bindingContext);
if (bindingContext.Result.IsModelSet == false)
{
var request = bindingContext.HttpContext.Request;
var body = request.Body;
request.EnableRewind();
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
request.Body.Read(buffer, 0, buffer.Length);
var bodyAsText = Encoding.UTF8.GetString(buffer);
var jobject = JObject.Parse(bodyAsText);
var value = jobject.GetValue(bindingContext.FieldName, StringComparison.InvariantCultureIgnoreCase);
var typeConverter = TypeDescriptor.GetConverter(bindingContext.ModelType);
var model = typeConverter.ConvertFrom(
context: null,
culture: CultureInfo.InvariantCulture,
value: value.ToString());
bindingContext.Result = ModelBindingResult.Success(model);
request.Body.Seek(0, SeekOrigin.Begin);
}
return result;
}
catch (Exception ex)
{
throw;
}
}
}
MyComplexTypeModelBinderProvider
public class MyComplexTypeModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
{
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
for (var i = 0; i < context.Metadata.Properties.Count; i++)
{
var property = context.Metadata.Properties[i];
propertyBinders.Add(property, context.CreateBinder(property));
}
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
return new MyComplexTypeModelBinder(
propertyBinders,
loggerFactory,
mvcOptions.AllowValidatingTopLevelNodes);
}
return null;
}
}
Register MyComplexTypeModelBinderProvider in Startup.cs
services.AddMvc(options => {
options.ModelBinderProviders.Insert(0, new MyComplexTypeModelBinderProvider());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
Actually you can have a single endpoint accepting multiple content-types:
[HttpPost]
[Consumes("application/json")]
public async Task<IActionResult> CommonEndpoint([FromBody] JObject payload)
{
...
}
[HttpPost]
[Consumes("x-www-form-urlencoded")]
public async Task<IActionResult> CommonEndpoint(/* this overload doesn't specify the payload in its signature */)
{
var formFieldsDictionary = HttpContext.Request.Form.Keys
.ToDictionary(k => k, k => HttpContext.Request.Form[k].FirstOrDefault());
var payload = JsonConvert.SerializeObject(formFieldsDictionary);
...
}
In .Net Core 2.2. I am creating a API Controller that routes the request to another Http endpoint based on payload.
[Route("api/v1")]
public class RoutesController : Controller
{
[HttpPost]
[Route("routes")]
public async Task<IActionResult> Routes([FromBody]JObject request)
{
var httpClient = new HttpClient();
// here based on request httpCLient will make `POST` or `GET` or `PUT` request
// and returns `Task<HttpResponseMessage>`. Lets assume its making `GET`
// call
Task<HttpResponseMessage> response = await
httpClient.GetAsync(request["resource"]);
/* ??? what is the correct way to return response as `IActionResult`*/
}
}
based on SO post i can do this
return StatusCode((int)response.StatusCode, response);
However i am not sure sending HttpResponseMessage as ObjectResult is correct way.
I also want to make sure content negotiation will work.
Update 7/25/2022
Updated the correct answer
public class HttpResponseMessageResult : IActionResult
{
private readonly HttpResponseMessage _responseMessage;
public HttpResponseMessageResult(HttpResponseMessage responseMessage)
{
_responseMessage = responseMessage; // could add throw if null
}
public async Task ExecuteResultAsync(ActionContext context)
{
var response = context.HttpContext.Response;
if (_responseMessage == null)
{
var message = "Response message cannot be null";
throw new InvalidOperationException(message);
}
using (_responseMessage)
{
response.StatusCode = (int)_responseMessage.StatusCode;
var responseFeature = context.HttpContext.Features.Get<IHttpResponseFeature>();
if (responseFeature != null)
{
responseFeature.ReasonPhrase = _responseMessage.ReasonPhrase;
}
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);
}
}
}
You can create a custom IActionResult that will wrap transfere logic.
public async Task<IActionResult> Routes([FromBody]JObject request)
{
var httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.GetAsync("");
// Here we ask the framework to dispose the response object a the end of the user resquest
this.HttpContext.Response.RegisterForDispose(response);
return new HttpResponseMessageResult(response);
}
public class HttpResponseMessageResult : IActionResult
{
private readonly HttpResponseMessage _responseMessage;
public HttpResponseMessageResult(HttpResponseMessage responseMessage)
{
_responseMessage = responseMessage; // could add throw if null
}
public async Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = (int)_responseMessage.StatusCode;
foreach (var header in _responseMessage.Headers)
{
context.HttpContext.Response.Headers.TryAdd(header.Key, new StringValues(header.Value.ToArray()));
}
if(_responseMessage.Content != null)
{
using (var stream = await _responseMessage.Content.ReadAsStreamAsync())
{
await stream.CopyToAsync(context.HttpContext.Response.Body);
await context.HttpContext.Response.Body.FlushAsync();
}
}
}
}
ASP.NET Core has the return object RedirectResult to redirect the caller.
Simply wrap the response in Ok() Action return type:
return Ok(response)
so your code would look something like:
[Route("api/v1")]
public class RoutesController : Controller
{
[HttpPost]
[Route("routes")]
public async Task<IActionResult> Routes([FromBody]JObject request)
{
var httpClient = new HttpClient();
Task<HttpResponseMessage> response = await httpClient.GetAsync(request["resource"]);
return Ok(response);
}
}
More info here: https://learn.microsoft.com/en-us/aspnet/core/web-api/action-return-types?view=aspnetcore-3.1
I am working on a simple .NET Core API with several endpoints.
I need to return the Content-Type in my Request as a vendor specific Media Type, "application/vnd.com.myhomepay.api.ste.location-code+json". However, whenever I set the Response.Headers.Add("Content-Type", "application/vnd.com.myhomepay.api.ste.location-code+json"), I get a 406 Status Code returned with the Response. I have read where I need to add this custom MediaTypeHeaderValue to the list of SupportedMediaTypes but I can't figure out how to do this in .Net Core. Any tips or suggestions for accomplishing this. I have also read about a more complicated approach using IOutputFormatter.
Each endpoint will have it's own vendor specific notation that will be passed back in the Response's Content-Type Header.
Option 1 that I tried:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.RespectBrowserAcceptHeader = true;
options.FormatterMappings.SetMediaTypeMappingForFormat("application/json", "application/vnd.com.myhomepay.api.ste.location-code+json;version=1");
options.FormatterMappings.SetMediaTypeMappingForFormat("application/json", "application/vnd.com.myhomepay.api.ste.tax-calculation+json;version=1");
});
}
Controller.cs
[Produces("application/vnd.com.myhomepay.api.ste.location-code+json;version=1")]
[HttpGet]
public IActionResult Get()
{
DailyReportAbbreviated dailyreport;
.....
return new OkObjectResult(dailyreport);
}
Request "Accept" header set to "application/vnd.com.myhomepay.api.ste.location-code+json;version=1"
This returns Status Cod 406 Not Acceptable
Option 2:
I just removed the "Produces" attribute from the controller and added:
Controller.cs
[HttpGet]
public IActionResult Get()
{
DailyReportAbbreviated dailyreport;
.....
Response.Headers.Add("Content-Type", "application/vnd.com.myhomepay.api.ste.location-code+json");
return new OkObjectResult(dailyreport);
}
This returns Status Cod 406 Not Acceptable
Option 3:
This option appears to be working
Starup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.RespectBrowserAcceptHeader = true;
options.OutputFormatters.Insert(0, new JsonOutputFormatter());
});
}
JsonOutputFormatter.cs
public class JsonOutputFormatter : IOutputFormatter
{
public bool CanWriteResult(OutputFormatterCanWriteContext context)
{
return true;
}
public async Task WriteAsync(OutputFormatterWriteContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));
var response = context.HttpContext.Response;
//context.ContentType returns the string set on the "Produces" attribute that is set on the controller
response.ContentType = context.ContentType.ToString();
using (var writer = context.WriterFactory(response.Body, Encoding.UTF8))
{
await writer.WriteAsync(JsonConvert.SerializeObject(context.Object));
}
}
}
Controller.cs
[Produces("application/vnd.com.myhomepay.api.ste.location-code+json;version=1")]
[HttpGet]
public IActionResult Get()
{
DailyReportAbbreviated dailyreport;
.....
return new OkObjectResult(dailyreport);
}
Using ASP.NET WebAPI 2.0 and have a conceptual issue.
Would like to keep a global record of any API that is called by any user/ client and it would be awesome if this was stored in the database.
What would be the best mechanism to accomplish this?
I' using a DelegatingHandler for a long time in several projects which is doing just fine.
public class ApiCallLogHandler : DelegatingHandler {
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
var started = DateTime.UtcNow;
HttpResponseMessage response = null;
Exception baseException = null;
try {
response = await base.SendAsync(request, cancellationToken);
} catch(Exception exception) {
CommonLogger.Logger.LogError(exception);
baseException = exception;
}
try {
var callModel = await GetCallModelAsync(request, response);
if(baseException != null)
callModel.Exception = baseException
callModel.ExecutionTime = (DateTime.UtcNow - started).ToString();
await CommonLogger.Logger.LogApiCallAsync(callModel);
} catch (Exception exception) {
CommonLogger.Logger.LogError(exception);
}
return response;
}
private async Task<ApiCallModel> GetCallModelAsync(HttpRequestMessage request, HttpResponseMessage response) {
// parse request and response and create a model to store in database...
}
}
By this approach you are able to track all requests, exceptions during execution, and even full-response of each API call.
ApiCallModel is just a simple POCO class which you should fill it with your required data from request and response.
CommonLogger.Logger.* is your logging mechanism.
And, you have to register the handler with this snippet:
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
config.MessageHandlers.Add(new ApiCallLogHandler());
}
}