in my service i have the following request:
[Route(#"/api/adddevice", Verbs = "POST")]
public class AddDeviceRequest : IReturn<AddDeviceResponse>
{
public DTOTargetDevice TargetDevice { get; set; }
}
It uses the following DTO:
public class DTOTargetDevice
{
private string _guid = string.Empty;
public string GUID
{
get { return _guid; }
set { _guid = DTOUtils.GetGUID(value); }
}
public string Type { get; set; }
public string DeviceName { get; set; }
public string[] PropertiesName { get; set; }
public string[] PropertiesValue { get; set; }
}
From a C# client, everything works fine, but when i try to call it, TargetDevice is always null.
This is the javascript code i'm using:
var parms = {};
parms.targetDevice = {};
parms.targetDevice.guid=guid();
parms.targetDevice.deviceName = guid();
parms.targetDevice.type = sDeviceType;
parms.targetDevice.propertiesName = (propsName);
parms.targetDevice.propertiesValue = (propsValue);
var config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8;'
}
}
var promise = $http.post("http://" + sIP + ":20001/api/adddevice", parms, config).then(function (response) {
var result = response.data.result;
console.log("Added Device: " + result);
return result;
});
This is the json, taken from Chrom Developer window:
{"targetDevice":{"guid":"e7461703-1b4b-7cd3-6263-3ac0935fb6f7","deviceName":"11e08b8f-d030-8f69-a469-4a1300aa49bf","type":"HTTPDevice","propertiesName":["Name","Acronym"],"propertiesValue":["Device","Device"]}}:
I've tried several options, but i'm always receiveng a null exception on TargetDevice within the request.
I've already read in other threads that simple objects is preferred, but in other services i have even more complex DTO.
Do you have any suggestion about that?
Thanks
Leo
I am quoting you:
This is the json, taken from Chrom Developer window:
The important word from your sentence is this: json. So make sure you specify the correct content type when making your request. You need to be consistent in what you claim to be sending and what you are actually sending to the server:
var config = {
headers: {
'Content-Type': 'application/json'
}
};
In your example you were claiming that you were sending application/x-www-form-urlencoded;charset=utf-8; and then dumping a JSON string, how do you expect the server to be able to cope with this confusion?
I guess that Angular or whatever this $http.post function that you are using will automatically JSON.stringify the parms object before sending it to the server. Maybe it will even automatically add the application/json content type request header, so you could completely get rid of this config variable of yours in this case.
Solved.
Instead of using in SetConfig()
GlobalResponseHeaders =
{
{ "Access-Control-Allow-Origin", "*" },
{ "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS" },
{ "Access-Control-Allow-Headers", "Content-Type" },
},
I've used:
Plugins.Add(new CorsFeature());
Thanks guys!
LEo
Related
After sending using [RelayCommand] in maui the setter in ViewModel receives truncated string in Maui.
Example orginal string: "https://twit.memberfulcontent.com/rss/9039?auth=m9FZurRandomAuthonumbers6yB"
The value of URL is good here:
[RelayCommand]
async Task Tap(string Url)
{
System.Diagnostics.Debug.WriteLine("Tap Sending: " + Url);
await Shell.Current.GoToAsync($"{nameof(ShowPage)}?Url={Url}");
}
when recieving here URL is truncated:
namespace NerdNewsNavigator2.ViewModel;
[QueryProperty("Url", "Url")]
public partial class ShowViewModel : ObservableObject
{
#region Properties
readonly TwitService _twitService;
public ObservableCollection<Show> Shows { get; set; } = new();
public string Url
{
set
{ // value is truncated string from Tap above.
System.Diagnostics.Debug.WriteLine("ShowViewModel Recieving url: " + value);
_ = GetShows(value);
OnPropertyChanged(nameof(Shows));
}
}
// Code shortened for brevity
Example of passed string:
"https://twit.memberfulcontent.com/rss/9039"
It gets truncated at ?Auth
Any suggestions on what I may be doing wrong? Or suggestion on a better way to do this? It works fine for string that do not have a ? mark in them. One hundred percent working except on this specific type of string.
I was expecting the string not to be truncated.
Correct way to pass data between pages in MAUI:
When you navigate:
Dictionary<string, object> query = new()
{
{ nameof(MyModel), myModel }
};
await Shell.Current.GoToAsync($"{nameof(MyPage)}", query);
When you are navigated:
public void ApplyQueryAttributes(IDictionary < string , object > query)
{
myModel = query[nameof(MyModel)] as MyModel;
}
(This solves more than the problem with sanitization)
So I have written a simple Azure Function (AF) that accepts (via Http Post method) an IFormCollection, loops through the file collection, pushes each file into an Azure Blob storage container and returns the url to each file.
The function itself works perfectly when I do a single file or multiple file post through Postman using the 'multipart/form-data' header. However when I try to post a file through an xUnit test, I get the following error:
System.IO.InvalidDataException : Multipart body length limit 16384 exceeded.
I have searched high and low for a solution, tried different things, namely;
Replicating the request object to be as close as possible to Postmans request.
Playing around with the 'boundary' in the header.
Setting 'RequestFormLimits' on the function.
None of these have helped so far.
The details are the project are as follows:
Azure Function v3: targeting .netcoreapp3.1
Startup.cs
public class Startup : FunctionsStartup
{
public IConfiguration Configuration { get; private set; }
public override void Configure(IFunctionsHostBuilder builder)
{
var x = builder;
InitializeConfiguration(builder);
builder.Services.AddSingleton(Configuration.Get<UploadImagesAppSettings>());
builder.Services.AddLogging();
builder.Services.AddSingleton<IBlobService,BlobService>();
}
private void InitializeConfiguration(IFunctionsHostBuilder builder)
{
var executionContextOptions = builder
.Services
.BuildServiceProvider()
.GetService<IOptions<ExecutionContextOptions>>()
.Value;
Configuration = new ConfigurationBuilder()
.SetBasePath(executionContextOptions.AppDirectory)
.AddJsonFile("appsettings.json")
.AddJsonFile("appsettings.Development.json", optional: true)
.AddEnvironmentVariables()
.Build();
}
}
UploadImages.cs
public class UploadImages
{
private readonly IBlobService BlobService;
public UploadImages(IBlobService blobService)
{
BlobService = blobService;
}
[FunctionName("UploadImages")]
[RequestFormLimits(ValueLengthLimit = int.MaxValue,
MultipartBodyLengthLimit = 60000000, ValueCountLimit = 10)]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "images")] HttpRequest req)
{
List<Uri> returnUris = new List<Uri>();
if (req.ContentLength == 0)
{
string badResponseMessage = $"Request has no content";
return new BadRequestObjectResult(badResponseMessage);
}
if (req.ContentType.Contains("multipart/form-data") && req.Form.Files.Count > 0)
{
foreach (var file in req.Form.Files)
{
if (!file.IsValidImage())
{
string badResponseMessage = $"{file.FileName} is not a valid/accepted Image file";
return new BadRequestObjectResult(badResponseMessage);
}
var uri = await BlobService.CreateBlobAsync(file);
if (uri == null)
{
return new ObjectResult($"Could not blob the file {file.FileName}.");
}
returnUris.Add(uri);
}
}
if (!returnUris.Any())
{
return new NoContentResult();
}
return new OkObjectResult(returnUris);
}
}
Exception Thrown:
The below exception is thrown at the second if statement above, when it tries to process req.Form.Files.Count > 0, i.e.
if (req.ContentType.Contains("multipart/form-data") && req.Form.Files.Count > 0) {}
Message:
System.IO.InvalidDataException : Multipart body length limit 16384 exceeded.
Stack Trace:
MultipartReaderStream.UpdatePosition(Int32 read)
MultipartReaderStream.ReadAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)
StreamHelperExtensions.DrainAsync(Stream stream, ArrayPool`1 bytePool, Nullable`1 limit, CancellationToken cancellationToken)
MultipartReader.ReadNextSectionAsync(CancellationToken cancellationToken)
FormFeature.InnerReadFormAsync(CancellationToken cancellationToken)
FormFeature.ReadForm()
DefaultHttpRequest.get_Form()
UploadImages.Run(HttpRequest req) line 42
UploadImagesTests.HttpTrigger_ShouldReturnListOfUploadedUris(String fileNames)
xUnit Test Project: targeting .netcoreapp3.1
Over to the xUnit Test project, basically I am trying to write an integration test. The project references the AF project and has the following classes:
TestHost.cs
public class TestHost
{
public TestHost()
{
var startup = new TestStartup();
var host = new HostBuilder()
.ConfigureWebJobs(startup.Configure)
.ConfigureServices(ReplaceTestOverrides)
.Build();
ServiceProvider = host.Services;
}
public IServiceProvider ServiceProvider { get; }
private void ReplaceTestOverrides(IServiceCollection services)
{
// services.Replace(new ServiceDescriptor(typeof(ServiceToReplace), testImplementation));
}
private class TestStartup : Startup
{
public override void Configure(IFunctionsHostBuilder builder)
{
SetExecutionContextOptions(builder);
base.Configure(builder);
}
private static void SetExecutionContextOptions(IFunctionsHostBuilder builder)
{
builder.Services.Configure<ExecutionContextOptions>(o => o.AppDirectory = Directory.GetCurrentDirectory());
}
}
}
TestCollection.cs
[CollectionDefinition(Name)]
public class TestCollection : ICollectionFixture<TestHost>
{
public const string Name = nameof(TestCollection);
}
HttpRequestFactory.cs: To create Http Post Request
public static class HttpRequestFactory
{
public static DefaultHttpRequest Create(string method, string contentType, Stream body)
{
var request = new DefaultHttpRequest(new DefaultHttpContext());
var contentTypeWithBoundary = new MediaTypeHeaderValue(contentType)
{
Boundary = $"----------------------------{DateTime.Now.Ticks.ToString("x")}"
};
var boundary = MultipartRequestHelper.GetBoundary(
contentTypeWithBoundary, (int)body.Length);
request.Method = method;
request.Headers.Add("Cache-Control", "no-cache");
request.Headers.Add("Content-Type", contentType);
request.ContentType = $"{contentType}; boundary={boundary}";
request.ContentLength = body.Length;
request.Body = body;
return request;
}
private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);
if (string.IsNullOrWhiteSpace(boundary.Value))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary.Value;
}
}
The MultipartRequestHelper.cs class is available here
And Finally the Test class:
[Collection(TestCollection.Name)]
public class UploadImagesTests
{
readonly UploadImages UploadImagesFunction;
public UploadImagesTests(TestHost testHost)
{
UploadImagesFunction = new UploadImages(testHost.ServiceProvider.GetRequiredService<IBlobService>());
}
[Theory]
[InlineData("testfile2.jpg")]
public async void HttpTrigger_ShouldReturnListOfUploadedUris(string fileNames)
{
var formFile = GetFormFile(fileNames);
var fileStream = formFile.OpenReadStream();
var request = HttpRequestFactory.Create("POST", "multipart/form-data", fileStream);
var response = (OkObjectResult)await UploadImagesFunction.Run(request);
//fileStream.Close();
Assert.True(response.StatusCode == StatusCodes.Status200OK);
}
private static IFormFile GetFormFile(string fileName)
{
string fileExtension = fileName.Substring(fileName.IndexOf('.') + 1);
string fileNameandPath = GetFilePathWithName(fileName);
IFormFile formFile;
var stream = File.OpenRead(fileNameandPath);
switch (fileExtension)
{
case "jpg":
formFile = new FormFile(stream, 0, stream.Length,
fileName.Substring(0, fileName.IndexOf('.')),
fileName)
{
Headers = new HeaderDictionary(),
ContentType = "image/jpeg"
};
break;
case "png":
formFile = new FormFile(stream, 0, stream.Length,
fileName.Substring(0, fileName.IndexOf('.')),
fileName)
{
Headers = new HeaderDictionary(),
ContentType = "image/png"
};
break;
case "pdf":
formFile = new FormFile(stream, 0, stream.Length,
fileName.Substring(0, fileName.IndexOf('.')),
fileName)
{
Headers = new HeaderDictionary(),
ContentType = "application/pdf"
};
break;
default:
formFile = null;
break;
}
return formFile;
}
private static string GetFilePathWithName(string filename)
{
var outputFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
return $"{outputFolder.Substring(0, outputFolder.IndexOf("bin"))}testfiles\\{filename}";
}
}
The test seems to be hitting the function and req.ContentLength does have a value. Considering this, could it have something to do with the way the File Streams are being managed? Perhaps not the right way?
Any inputs on this would be greatly appreciated.
Thanks
UPDATE 1
As per this post, I have also tried setting the ValueLengthLimit and MultipartBodyLengthLimit in the Startup of the Azure Function and/or the Test Project as opposed to attributes on the Azure Function. The exception then changed to:
"The inner stream position has changed unexpectedly"
Following this, I then set the fileStream position in the test project to SeekOrigin.Begin. I started getting the same error:
"Multipart body length limit 16384 exceeded."
It took me a 50km bike ride and a good nights sleep but I finally figured this one out :-).
The Azure function (AF) accepts an HttpRequest object as a parameter with the name of 'req' i.e.
public async Task Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "images")] HttpRequest req)
The hierarchy of the files object in the HttpRequest object (along with the parameter names) is as follows:
HttpRequest -> req
FormCollection -> Form
FormFileCollection -> Files
This is what the AF accepts and one would access the files collection by using req.Form.Files
In my test case, instead of posting a FormCollection object, I was trying to post a Stream of a file to the Azure Function.
var formFile = GetFormFile(fileNames);
var fileStream = formFile.OpenReadStream();
var request = HttpRequestFactory.Create("POST", "multipart/form-data", fileStream);
As a result of this, req.Form had a Stream value that it could not interpret and the req.Form.Files was raising an exception.
In order to rectify this, I had to do the following:
Revert all changes made as part of UPDATE 1. This means that I removed the 'RequestFormLimits' settings from the Startup file and left them as attributes on the functions Run method.
Instantiate a FormFileCollection object and add the IFormFile to it
Instantiate a FormCollection object using this FormFileCollection as a parameter.
Add the FormCollection to the request object.
To achieve the above, I had to make the following changes in code.
Change 'Create' method in the HttpRequestFactory
public static DefaultHttpRequest Create(string method, string contentType, FormCollection formCollection)
{
var request = new DefaultHttpRequest(new DefaultHttpContext());
var boundary = $"----------------------------{DateTime.Now.Ticks.ToString("x")}";
request.Method = method;
request.Headers.Add("Cache-Control", "no-cache");
request.Headers.Add("Content-Type", contentType);
request.ContentType = $"{contentType}; boundary={boundary}";
request.Form = formCollection;
return request;
}
Add a private static GetFormFiles() method
I wrote an additional GetFormFiles() method that calls the existing GetFormFile() method, instantiate a FormFileCollection object and add the IFormFile to it. This method in turn returns a FormFileCollection.
private static FormFileCollection GetFormFiles(string fileNames)
{
var formFileCollection = new FormFileCollection();
foreach (var file in fileNames.Split(','))
{
formFileCollection.Add(GetFormFile(file));
}
return formFileCollection;
}
Change the Testmethod
The test method calls the GetFormFiles() to get a FormFileCollection then
instantiates a FormCollection object using this FormFileCollection as a parameter and then passes the FormCollection object as a parameter to the HttpRequest object instead of passing a Stream.
[Theory]
[InlineData("testfile2.jpg")]
public async void HttpTrigger_ShouldReturnListOfUploadedUris(string fileNames)
{
var formFiles = GetFormFiles(fileNames);
var formCollection = new FormCollection(null, formFiles);
var request = HttpRequestFactory.Create("POST", "multipart/form-data", formCollection);
var response = (OkObjectResult) await UploadImagesFunction.Run(request);
Assert.True(response.StatusCode == StatusCodes.Status200OK);
}
So in the end the issue was not really with the 'RequestFormLimits' but rather with the type of data I was submitting in the POST message.
I hope this answer provides a different perspective to someone that comes across the same error message.
Cheers.
I'm trying to perform a patch with a JsonServiceClient to a service stack api as follows:
var patchRequest = new JsonPatchRequest
{
new JsonPatchElement
{
op = "replace",
path = "/firstName",
value = "Test"
}
};
_jsonClient.Patch<object>($"/testurl/{id}", patchRequest);
But I'm getting the following error:
Content-Type must be 'application/json-patch+json'
The error is clear. Is there a way to change the content type before perform the request for the JsonServiceClient?
This is the request POCO in the ServiceStack api:
[Api("Partial update .")]
[Route("/testurl/{Id}”, "PATCH")]
public class PartialTest : IReturn<PartialTestRequestResponse>, IJsonPatchDocumentRequest,
IRequiresRequestStream
{
[ApiMember(Name = “Id”, ParameterType = "path", DataType = "string", IsRequired = true)]
public string Id { get; set; }
public Stream RequestStream { get; set; }
}
public class PartialTestRequestResponse : IHasResponseStatus
{
public ResponseStatus ResponseStatus { get; set; }
}
Service implementation:
public object Patch(PartialTest request)
{
var dbTestRecord = Repo.GetDbTestRecord(request.Id);
if (dbTestRecord == null) throw HttpError.NotFound("Record not found.");
var patch =
(JsonPatchDocument<TestRecordPoco>)
JsonConvert.DeserializeObject(Request.GetRawBody(), typeof(JsonPatchDocument<TestRecordPoco>));
if (patch == null)
throw new HttpError(HttpStatusCode.BadRequest, "Body is not a valid JSON Patch Document.");
patch.ApplyTo(dbTestRecord);
Repo.UpdateDbTestRecord(dbTestRecord);
return new PartialTestResponse();
}
I'm using Marvin.JsonPatch V 1.0.0 library.
It's still not clear where the Exception is coming from as it's not an Error within ServiceStack. If you've registered a Custom Format or Filter that throws this error please include its impl (or a link to it) as well as the full StackTrace which will identify the source of the error.
But you should never call Patch<object> as an object return type doesn't specify what Response Type to deserialize into. Since you have an IReturn<T> marker you can just send the Request DTO:
_jsonClient.Patch(new PartialTest { ... });
Which will try to deserialize the Response in the IReturn<PartialTestRequestResponse> Response DTO. But as your Request DTO implements IRequiresRequestStream it's saying you're expecting unknown bytes that doesn't conform to a normal Request DTO, in which case you likely want to use a raw HTTP Client like HTTP Utils, e.g:
var bytes = request.Url.SendBytesToUrl(
method: HttpMethods.Path,
requestBody: jsonPatchBytes,
contentType: "application/json-patch+json",
accept: MimeTypes.Json);
You could modify the ContentType of a JSON Client using a request filter, e.g:
_jsonClient.RequestFilter = req =>
req.ContentType = "application/json-patch+json";
But it's more appropriate to use a low-level HTTP Client like HTTP Utils for non-JSON Service Requests like this.
I've got a service setup using the CorsFeature, and am using the approach that mythz suggested in other answers, collected in a function used in the appHost file:
private void ConfigureCors(Funq.Container container)
{
Plugins.Add(new CorsFeature(allowedOrigins: "*",
allowedMethods: "GET, POST, PUT, DELETE, OPTIONS",
allowedHeaders: "Content-Type, Authorization, Accept",
allowCredentials: true));
PreRequestFilters.Add((httpReq, httpRes) =>
{
//Handles Request and closes Responses after emitting global HTTP Headers
if (httpReq.HttpMethod == "OPTIONS")
{
httpRes.EndRequest();
}
});
}
However, the pre-request filter is only firing on some of the service requests. One of the base entities we have in the service is a question entity, and there are custom routes defined as follows:
[Route("/question")]
[Route("/question/{ReviewQuestionId}", "GET,DELETE")]
[Route("/question/{ReviewQuestionId}/{ReviewSectionId}", "GET")]
Using POSTMAN to fire test queries (all using the OPTIONS verb), we can see that this will fire the pre-request filter:
http://localhost/myservice/api/question/
But this will not:
http://localhost/myservice/api/question/66
Presumably, this is because the second and third routes explicitly defined the verbs they accept, and OPTIONS isn't one of them.
Is it really necessary to spell out OPTIONS in every defined route that restricts the verbs supported?
The PreRequestFilters is only fired for valid routes which doesn't exclude OPTIONS (e.g. by leaving Verbs=null and allow it to handle all Verbs instead - inc. OPTIONS).
To be able to handle all OPTIONS requests (i.e. even for non-matching routes) you would need to handle the Request at the start of the Request pipeline (i.e. before Routes are matched) with Config.RawHttpHandlers. This is done in the CorsFeature for you in the next major (v4) release of ServiceStack with:
//Handles Request and closes Response after emitting global HTTP Headers
var emitGlobalHeadersHandler = new CustomActionHandler(
(httpReq, httpRes) => httpRes.EndRequest());
appHost.RawHttpHandlers.Add(httpReq =>
httpReq.HttpMethod == HttpMethods.Options
? emitGlobalHeadersHandler
: null);
CustomActionHandler doesn't exist in v3, but it's easily created with:
public class CustomActionHandler : IServiceStackHttpHandler, IHttpHandler
{
public Action<IHttpRequest, IHttpResponse> Action { get; set; }
public CustomActionHandler(Action<IHttpRequest, IHttpResponse> action)
{
if (action == null)
throw new Exception("Action was not supplied to ActionHandler");
Action = action;
}
public void ProcessRequest(IHttpRequest httpReq, IHttpResponse httpRes, string operationName)
{
Action(httpReq, httpRes);
}
public void ProcessRequest(HttpContext context)
{
ProcessRequest(context.Request.ToRequest(GetType().Name),
context.Response.ToResponse(),
GetType().Name);
}
public bool IsReusable
{
get { return false; }
}
}
Using Fallback handler
Another way to match all Routes is to specify a FallbackRoute, e.g to handle all routes you can add a wildcard to the Fallback route with:
[FallbackRoute("/{Path*}")]
public class Fallback
{
public string Path { get; set; }
}
But as it matches all un-handled routes it no longer gives 404 for non-matching requests since all un-matched routes are now matched. But you can easily handle it manually with:
public class FallbackService : Service
{
public object Any(Fallback request)
{
if (base.Request.HttpMethod == "OPTIONS")
return null;
throw HttpError.NotFound("{0} was not found".Fmt(request.Path));
}
}
You don't have to add the OPTIONS verb to all routes. Instead you can do the following:
Just put this route on your Question class:
[Route("/question/{*}", Verbs = "OPTIONS")]
public class Question
{
}
Then add this to your question services class:
public void Options(Question question)
{
}
Now any route starting with /question/ will support the OPTIONS verb.
You might want to restrict this if you're going to have subroutes like /question/something/ though.
Following steps worked for me within ServiceStackV3.
1. Added a new class CustomActionHandler
using ServiceStack.ServiceHost;
using ServiceStack.WebHost.Endpoints.Extensions;
using ServiceStack.WebHost.Endpoints.Support;
public class CustomActionHandler : IServiceStackHttpHandler, IHttpHandler
{
public Action<IHttpRequest, IHttpResponse> Action { get; set; }
public CustomActionHandler(Action<IHttpRequest, IHttpResponse> action)
{
if (action == null)
throw new Exception("Action was not supplied to ActionHandler");
Action = action;
}
public void ProcessRequest(IHttpRequest httpReq, IHttpResponse httpRes, string operationName)
{
Action(httpReq, httpRes);
}
public void ProcessRequest(HttpContext context)
{
ProcessRequest(context.Request.ToRequest(GetType().Name),
context.Response.ToResponse(),
GetType().Name);
}
public bool IsReusable
{
get { return false; }
}
}
2. Add the CustomHandler within AppHostBase.Config.RawHttpHandlers collection (this statements can be written inside the Configure(Container container) method).
// Handles Request and closes Response after emitting global HTTP Headers
var emitGlobalHeadersHandler = new CustomActionHandler((httpReq, httpRes) => httpRes.EndRequest());
Config.RawHttpHandlers.Add(httpReq => httpReq.HttpMethod == HttpMethods.Options ? emitGlobalHeadersHandler : null);
I have setup a web api to allow cross domain access with Basic Authentication. When I make a cross domain GET request to the API, it works fine and I am getting token in "Authorization" header in my custom message handler. But when initiating a cross domain POST request, I am not getting the "Authorization" header that's why unable to validate the request.
Any help would be highly appreciated.
Following is the code for my custom message handler for cross domain access.
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace MyWebApi.Handlers
{
public class XHttpMethodOverrideDelegatingHandler : DelegatingHandler
{
static readonly string[] HttpOverrideMethods = { "PUT", "DELETE" };
static readonly string[] AccessControlAllowMethods = { "POST", "PUT", "DELETE" };
private const string HttpMethodOverrideHeader = "X-HTTP-Method-Override";
private const string OriginHeader = "ORIGIN";
private const string AccessControlAllowOriginHeader = "Access-Control-Allow-Origin";
private const string AccessControlAllowMethodsHeader = "Access-Control-Allow-Methods";
private const string AccessControlAllowHeadersHeader = "Access-Control-Allow-Headers";
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var authHeader = request.Headers.Authorization;
if (authHeader == null || authHeader.Scheme != "Basic" || string.IsNullOrWhiteSpace(authHeader.Parameter))
{
return CreateUnauthorizedResponse();
}
if (request.Method == HttpMethod.Post && request.Headers.Contains(HttpMethodOverrideHeader))
{
var httpMethod = request.Headers.GetValues(HttpMethodOverrideHeader).FirstOrDefault();
if (HttpOverrideMethods.Contains(httpMethod, StringComparer.InvariantCultureIgnoreCase))
request.Method = new HttpMethod(httpMethod);
}
var httpResponseMessage = base.SendAsync(request, cancellationToken);
if (request.Method == HttpMethod.Options && request.Headers.Contains(OriginHeader))
{
httpResponseMessage.Result.Headers.Add(AccessControlAllowOriginHeader, request.Headers.GetValues(OriginHeader).FirstOrDefault());
httpResponseMessage.Result.Headers.Add(AccessControlAllowMethodsHeader, String.Join(", ", AccessControlAllowMethods));
httpResponseMessage.Result.Headers.Add(AccessControlAllowHeadersHeader, HttpMethodOverrideHeader);
httpResponseMessage.Result.StatusCode = HttpStatusCode.OK;
}
//No mater what the HttpMethod (POST, PUT, DELETE), if a Origin Header exists, we need to take care of it
else if (request.Headers.Contains(OriginHeader))
{
httpResponseMessage.Result.Headers.Add(AccessControlAllowOriginHeader, request.Headers.GetValues(OriginHeader).FirstOrDefault());
}
return httpResponseMessage;
}
private Task<HttpResponseMessage> CreateUnauthorizedResponse()
{
var response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.Headers.Add("WWW-Authenticate", "Basic");
var taskCompletionSource = new TaskCompletionSource<HttpResponseMessage>();
taskCompletionSource.SetResult(response);
return taskCompletionSource.Task;
}
}
}
And i have registered the above handler in Application_Start as follows:
namespace MyWebApi
{
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new {id = RouteParameter.Optional});
GlobalConfiguration.Configuration.MessageHandlers.Add(new XHttpMethodOverrideDelegatingHandler());
GlobalConfiguration.Configuration.Formatters.Insert(0, new JsonpMediaTypeFormatter());
}
}
}
At client side on a different domain project, I am trying to add a new record using following code.
AddUser {
var jsonData = {
"FirstName":"My First Name",
"LastName": "My Last Name",
"Email": "my.name#mydomain.com",
"Password": "MyPa$$word"
};
$.ajax({
type: "POST",
dataType: 'json',
url: "http://localhost:4655/api/user/signup",
beforeSend: function (xhr) { xhr.setRequestHeader("Authorization", "Basic xxxxxxxxxxxxxx"); },
accept: "application/json",
data: JSON.stringify(jsonData),
success: function (data) {
alert("success");
},
failure: function (errorMsg) {
alert(errorMsg);
},
error: function (onErrorMsg) {
alert(onErrorMsg.statusText);
},
statusCode: function (test) {
alert("status");
}
});
});
And following is the code for my user controller.
namespace MyWebApi.Controllers
{
public class UserController : ApiController
{
[HttpPost]
[ActionName("Adduser")]
public int Post(UserModel source)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
Db.Users.Add(source);
Db.SaveChanges();
return source.UserId;
}
}
}
Thanks in advance!
I've discovered that if I include basic auth credentials in my cross-domain (POST) XHR request, the browser (IE, Chrome, Firefox) rejects the request before it ever gets to my server - and this is true even if I specify withCredentials:true in my initial $.ajax() request. I'm guessing that there's probably something in the CORS spec which requires this. But I think the short answer is that you can't specify basic auth in CORS requests.
Of course, you can get around this in other ways, by passing userids and passwords as part of the URL proper, so I'm not entirely clear what they think they're gaining by restricting it, but presumably they have some reason.
You need to decorate your controller with [HttpOptions]as well as [HttpPost]. Otherwise when it makes a request using the OPTIONS verb, it will throw a 404. So your controller would be
[HttpPost]
[HttpOptions]
[ActionName("Adduser")]
public int Post(UserModel source)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
Db.Users.Add(source);
Db.SaveChanges();
return source.UserId;
}