I implemented a simple server with .Net5 and an Angular frontend. My api works with post man but only if I set Content-Type equal to application/x-www-form-urlencoded. When I use Content-Type equal to application/json the server receives empty json. When I use my frontend I think I send to server an application/json because it receives the empty one. Below my code.
api.service.ts
public createProva(prova:Iprova): Observable<Iprova> {
return this.http.post<Iprova>( this.Url, prova );
}
When I console.log prova it return a correct Json
controller.cs
[HttpPost]
[Route("url")]
public async Task<IActionResult> postProva(provaDto s)
{
porvaModel prova = new provaModel
{
Id = s.id,
Name = s.name
};
_context.provaModel.Add(prova);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(getProva), new { id = prova.Id }, prova);
}
but when backend receives provaDto, s is empty with id equal to 0 and name equal to null.
For simplicity, I assume you have a basic setup for your controller such as this :
[ApiController]
[Route("[controller]")]
public class ProvaController : ControllerBase
I also disable the https redirection in the startup.cs for development purposes, while keeping the default routing functionality.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (!env.IsDevelopment())
{
app.UseHttpsRedirection();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
I'll keep the same post method:
[HttpPost]
public async Task<IActionResult> postProva(provaDto s)
{
porvaModel prova = new provaModel
{
Id = s.id,
Name = s.name
};
_context.provaModel.Add(prova);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(getProva), new { id = prova.Id }, prova);
}
As for the Postman setup, I use a POST request on http://localhost:5000/Prova (I use the default launchSettings using dotnet CLI), I also keep default headers. In the Body, I'll use raw and select JSON format, giving it a simple payload :
{
"id": 0,
"name": "string"
}
As for the angular part, you can specify the content-type of your resquest with the httpOptions :
httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
})
};
public createProva(prova:Iprova): Observable<Iprova> {
return this.http.post<Iprova>( this.Url, prova , this.httpOptions);
}
Related
I'm developing ASP Core Web API using dotnet core v3.1.
I'm using JWT tokens for authentication. And for authorization I use the [Authorize] attribute.
How can I create my own response if the user is not logged in (while trying to access the action marked with the [Authorize] attribute) or the user's token is not authenticated.
I came across a solution using a custom authorization attribute inherited from the default one. And in this example, the HandleUnauthorizedRequest method is overridden. But I don't see such a method inside the AuthorizeAttribute class.
Is there a way to create custom unauthorized responses with http body?
Since you are using JWT bearer authentication, one way to override the default Challenge logic (which executes to handle 401 Unauthorized concerns) is to hook a handler to the JwtBearerEvents.OnChallenge callback in Startup.ConfigureServices:
services.AddAuthentication().AddJwtBearer(options =>
{
// Other configs...
options.Events = new JwtBearerEvents
{
OnChallenge = async context =>
{
// Call this to skip the default logic and avoid using the default response
context.HandleResponse();
// Write to the response in any way you wish
context.Response.StatusCode = 401;
context.Response.Headers.Append("my-custom-header", "custom-value");
await context.Response.WriteAsync("You are not authorized! (or some other custom message)");
}
};
});
This will override the default challenge logic in JwtBearerHandler.HandleChallengeAsync, which you can find here for reference purposes.
The default logic does not write any content to response (it only sets the status code and set some headers). So to keep using the default logic and add content on top of it, you can use something like this:
options.Events = new JwtBearerEvents
{
OnChallenge = context =>
{
context.Response.OnStarting(async () =>
{
// Write to the response in any way you wish
await context.Response.WriteAsync("You are not authorized! (or some other custom message)");
});
return Task.CompletedTask;
}
};
For .net core 5 web api project with jwt authentication use this middleware in Configure method of Startup.cs for show ErrorDto in Swagger:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "LoginService v1"));
}
app.ConfigureExceptionHandler();
app.UseHttpsRedirection();
app.UseRouting();
// Unauthorized (401) MiddleWare
app.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized) // 401
{
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(new ErrorDto()
{
StatusCode = 401,
Message = "Token is not valid"
}.ToString());
}
});
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
ErrorDto :
public class ErrorDto
{
public int StatusCode { get; set; }
public string Message { get; set; }
public override string ToString()
{
return JsonSerializer.Serialize(this);
}
}
This is what I came up with for responding with the same ProblemDetails you would get from returning Unauthorized() in an ApiController:
.AddJwtBearer(options =>
{
// Other configs...
options.Events = new JwtBearerEvents
{
OnChallenge = async context =>
{
// Call this to skip the default logic and avoid using the default response
context.HandleResponse();
var httpContext = context.HttpContext;
var statusCode = StatusCodes.Status401Unauthorized;
var routeData = httpContext.GetRouteData();
var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());
var factory = httpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();
var problemDetails = factory.CreateProblemDetails(httpContext, statusCode);
var result = new ObjectResult(problemDetails) { StatusCode = statusCode };
await result.ExecuteResultAsync(actionContext);
}
};
});
I have an array that I'm converting to JSON using JSON.stringify
const arrayOfUpdatesAsJSON = JSON.stringify(this.ArrayOfTextUpdates);
This outputs some valid JSON.
[{"key":"AgentName","value":"Joe Blogs"},{"key":"AgentEmail","value":"Joe#test.com"}]
As I'm going to be sending JSON to the server I set the content type to application/json
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
})
};
When a button is pressed I make the request with the url, body and header.
try {
this.httpservice
.post(
url,
arrayOfUpdatesAsJSON,
httpOptions
)
.subscribe(result => {
console.log("Post success: ", result);
});
} catch (error) {
console.log(error);
}
This works fine and hits the method I'm expecting inside the api.
[HttpPost("{id:length(24)}", Name = "UpdateLoan")]
public IActionResult Update(string id, string jsonString)
{
Console.WriteLine(jsonString);
... and some other stuff
}
The ID is populated inside the url builder which populates ok. I would then expect the contents of my variable jsonString inside the api to be populated with the json of my request however it is always null. What am I missing?
Firstly you need to mark jsonString with [FromBody] to tell model binder bind the parameter from posted json. And because you are expecting plain string value you need to pass valid json string (not object) so you need to call additional JSON.stringify in javascript
const jsonArray = JSON.stringify(this.ArrayOfTextUpdates);
const arrayOfUpdatesAsJSON = JSON.stringify(jsonArray);
this.httpservice
.post(
url,
arrayOfUpdatesAsJSON,
httpOptions
)
Controller
[HttpPost("{id:length(24)}", Name = "UpdateLoan")]
public IActionResult Update(string id, [FromBody] string jsonString)
I'm trying to perform a post request to my WebAPI controller, but there is no way to make my action to be called.
This is the action:
[HttpPost("about")]
public async Task<IActionResult> PostAbout([FromBody] PostAboutBindingModel model)
{
if (model == null || !ModelState.IsValid)
return BadRequest(ModelState);
var about = new About
{
Text = model.Text,
Date = model.Date,
Images = _jsonSerializer.Serialize(model.Images)
};
_context.Abouts.Add(about);
await _context.SaveChangesAsync();
return Created($"/api/about/{about.Version}", about);
}
The PostAboutBindingModel has only three properties: Text, Date and Images.
This is the angular2 code snippet where I perform the API call:
let model: IAbout = <IAbout>{
date: new Date(),
images: [],
text: "test"
}
let opts = jwtAuthorization();
opts.headers.append("Content-Type", "application/json");
return this.http.post("/api/about", model, opts)
.map((response: Response) => console.log("TEST", response.json()))
.catch(this.handleError);
The jwtAuthorization simply add the Authorization header:
export function jwtAuthorization(): RequestOptions {
"use strict"
if (localStorage.getItem("auth")) {
// create authorization header with jwt token
let auth: IAuth = getAuth(JSON.parse(atob(localStorage.getItem("auth"))));
if (auth && auth.access_token) {
let headers: Headers = new Headers({ "Authorization": auth.token_type + " " + auth.access_token });
return new RequestOptions({ headers: headers });
}
}
}
I've tried to specify, as body, the following things:
model
{ model }
{ model: model }
JSON.stringify(model)
JSON.stringify({ model: model })
I've tried to specify my model as a generic object (without type) too.
I've tried to perform the call with and without the Content-Type header.
None of the previous seems to work. The API action is not called and no errors are returned.
I would like to perform the request specify only model as-is if it's possible but I would be happy in any case, if it works :)
What am I missing?
EDIT
I read now that http requests in angular 2 are "lazy" so they need a subscriber (subscribe) to work.
Thanks for help
I want to post a file with some JSON data using Spring MVC. So I've developed a rest service as
#RequestMapping(value = "/servicegenerator/wsdl", method = RequestMethod.POST,consumes = { "multipart/mixed", "multipart/form-data" })
#ResponseBody
public String generateWSDLService(#RequestPart("meta-data") WSDLInfo wsdlInfo,#RequestPart("file") MultipartFile file) throws WSDLException, IOException,
JAXBException, ParserConfigurationException, SAXException, TransformerException {
return handleWSDL(wsdlInfo,file);
}
When I send a request from the rest client with
content-Type = multipart/form-data or multipart/mixed, I get the next exception:
org.springframework.web.multipart.support.MissingServletRequestPartException
Can anyone help me in solving this issue?
Can I use #RequestPart to send both Multipart and JSON to a server?
This is how I implemented Spring MVC Multipart Request with JSON Data.
Multipart Request with JSON Data (also called Mixed Multipart):
Based on RESTful service in Spring 4.0.2 Release, HTTP request with the first part as XML or JSON formatted data and the second part as a file can be achieved with #RequestPart. Below is the sample implementation.
Java Snippet:
Rest service in Controller will have mixed #RequestPart and MultipartFile to serve such Multipart + JSON request.
#RequestMapping(value = "/executesampleservice", method = RequestMethod.POST,
consumes = {"multipart/form-data"})
#ResponseBody
public boolean executeSampleService(
#RequestPart("properties") #Valid ConnectionProperties properties,
#RequestPart("file") #Valid #NotNull #NotBlank MultipartFile file) {
return projectService.executeSampleService(properties, file);
}
Front End (JavaScript) Snippet:
Create a FormData object.
Append the file to the FormData object using one of the below steps.
If the file has been uploaded using an input element of type "file", then append it to the FormData object.
formData.append("file", document.forms[formName].file.files[0]);
Directly append the file to the FormData object.
formData.append("file", myFile, "myfile.txt"); OR formData.append("file", myBob, "myfile.txt");
Create a blob with the stringified JSON data and append it to the FormData object. This causes the Content-type of the second part in the multipart request to be "application/json" instead of the file type.
Send the request to the server.
Request Details:
Content-Type: undefined. This causes the browser to set the Content-Type to multipart/form-data and fill the boundary correctly. Manually setting Content-Type to multipart/form-data will fail to fill in the boundary parameter of the request.
Javascript Code:
formData = new FormData();
formData.append("file", document.forms[formName].file.files[0]);
formData.append('properties', new Blob([JSON.stringify({
"name": "root",
"password": "root"
})], {
type: "application/json"
}));
Request Details:
method: "POST",
headers: {
"Content-Type": undefined
},
data: formData
Request Payload:
Accept:application/json, text/plain, */*
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryEBoJzS3HQ4PgE1QB
------WebKitFormBoundaryvijcWI2ZrZQ8xEBN
Content-Disposition: form-data; name="file"; filename="myfile.txt"
Content-Type: application/txt
------WebKitFormBoundaryvijcWI2ZrZQ8xEBN
Content-Disposition: form-data; name="properties"; filename="blob"
Content-Type: application/json
------WebKitFormBoundaryvijcWI2ZrZQ8xEBN--
This must work!
client (angular):
$scope.saveForm = function () {
var formData = new FormData();
var file = $scope.myFile;
var json = $scope.myJson;
formData.append("file", file);
formData.append("ad",JSON.stringify(json));//important: convert to JSON!
var req = {
url: '/upload',
method: 'POST',
headers: {'Content-Type': undefined},
data: formData,
transformRequest: function (data, headersGetterFunction) {
return data;
}
};
Backend-Spring Boot:
#RequestMapping(value = "/upload", method = RequestMethod.POST)
public #ResponseBody
Advertisement storeAd(#RequestPart("ad") String adString, #RequestPart("file") MultipartFile file) throws IOException {
Advertisement jsonAd = new ObjectMapper().readValue(adString, Advertisement.class);
//do whatever you want with your file and jsonAd
You can also use the next way a list List<MultipartFile> and #RequestPart("myObj") as parameters in your method inside a #RestController
#PostMapping()
#ResponseStatus(HttpStatus.CREATED)
public String create(#RequestPart("file") List<MultipartFile> files, #RequestPart("myObj") MyJsonDTOClass myObj) throws GeneralSecurityException, IOException {
// your code
}
and in the axios side with a bit of react:
const jsonStr = JSON.stringify(myJsonObj);
const blob = new Blob([jsonStr], {
type: 'application/json'
});
let formData = new FormData();
formData.append("myObj",blob );
formData.append("file", this.state.fileForm); // check your control
let url = `your url`
let method = `post`
let headers =
{
'Accept': 'application/json',
'Content-Type': 'application/json'
};
}
axios({
method,
url,
data: formData,
headers
}).then(res => {
console.log(res);
console.log(res.data);
});
We've seen in our projects that a post request with JSON and files is creating a lot of confusion between the frontend and backend developers, leading to unnecessary wastage of time.
Here's a better approach: convert file bytes array to Base64 string and send it in the JSON.
public Class UserDTO {
private String firstName;
private String lastName;
private FileDTO profilePic;
}
public class FileDTO {
private String base64;
// just base64 string is enough. If you want, send additional details
private String name;
private String type;
private String lastModified;
}
#PostMapping("/user")
public String saveUser(#RequestBody UserDTO user) {
byte[] fileBytes = Base64Utils.decodeFromString(user.getProfilePic().getBase64());
....
}
JS code to convert file to base64 string:
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function () {
const userDTO = {
firstName: "John",
lastName: "Wick",
profilePic: {
base64: reader.result,
name: file.name,
lastModified: file.lastModified,
type: file.type
}
}
// post userDTO
};
reader.onerror = function (error) {
console.log('Error: ', error);
};
As documentation says:
Raised when the part of a "multipart/form-data" request identified by
its name cannot be found.
This may be because the request is not a multipart/form-data either
because the part is not present in the request, or because the web
application is not configured correctly for processing multipart
requests -- e.g. no MultipartResolver.
For Angular2+ users. Try to send JSON payload in a mixed part request as below.
formData.append("jsonPayload", new Blob([JSON.stringify(json)], {
type: "application/json"
}));
Given below complete function.
submit() {
const formData = new FormData();
formData.append('file', this.myForm.get('fileSource').value);
var json = {
"key":"value"
};
formData.append("jsonPayload", new Blob([JSON.stringify(json)], {
type: "application/json"
}));
this.http.post('http://localhost:8080/api/mixed-part-endpoint', formData)
.subscribe(res => {
console.log(res);
alert('Uploaded Successfully.');
})
}
I've run into a problem while building a new MVC WebApi project where my post actions do not appear to be working correctly.
I have the following action:
//--
//-- POST: /api/groups/subscribe/1/groups
[HttpPost]
public GroupResponse Subscribe(int id, List<int> groups )
{
var response = new GroupResponse();
var manager = new UserManagement();
try
{
response.Status = 1;
var subscribedGroups = manager.GetSubscribedGroups(id).Select(g => g.GroupId).ToList();
foreach (var subscribedGroup in subscribedGroups.Where(groups.Contains))
{
groups.Remove(subscribedGroup);
}
//-- group is a reserved word # escapes this and treats it as a regular variable
foreach (var #group in groups.Where(g => !manager.JoinGroup(id, g)))
{
response.Status = 2;
response.ErrorMessage = Constants.SUBSCRIBE_FAIL;
}
}
catch (Exception)
{
response.Status = 2;
response.ErrorMessage = Constants.SUBSCRIBE_FAIL;
return response;
}
return response;
}
When I attempt to consume this action from rest kit I get the following error message:
{
"Message":"No HTTP resource was found that matches the request URI 'http://localhost:50393/api/groups/subscribe'.",
"MessageDetail":"No action was found on the controller 'Groups' that matches the request."
}
I've tried executing the action via fiddler however it looks like the api is ignoring my data being sent to the api which is confusing me at the moment.
When I attempt to use the api as follows: /api/groups/subscribe?id=1 then the api action is executed, however I'm unable to pass the the required list.
I've also setup a route to try and handle this, but it doesn't appear to be helping out at all:
config.Routes.MapHttpRoute(
"subscribe",
"api/groups/subscribe/{id}/{groups}",
new { controller = "Groups", action = "Subscribe", id = RouteParameter.Optional, groups = RouteParameter.Optional
});
Additional info:
When testing with fiddler I am composing my own requests as follows:
Request Headers:
User-Agent: Fiddler
Host: localhost:50393
Content-Length: 29
Content-Type: application/json; charset=utf-8
Request Body:
{"id":1,"groups":[1,2,3,4,5]}
You can try this, tested now with fiddler.
The controller:
public class AuthenticationController : ApiController
{
[AcceptVerbs("POST")]
public void Subscribe(SubscribeClass data)
{
//Do your stuff here
int id = data.id;
List<int> groups = data.groups;
//data contains values
}
public class SubscribeClass
{
public int id { get; set; }
public List<int> groups { get; set; }
}
}
The WebApiConfig:
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "AuthenticateUser",
routeTemplate: "{controller}/{action}",
defaults: new { },
constraints: new { controller = "Authentication", action = "Subscribe" }
);
}
The JSON object send via Fiddler:
{ "id": 1 , "groups": [1, 2, 3, 4, 5] }
In the Headers section in Fiddler make sure to add this header for your scenario:
Content-Type: application/json; charset=utf-8
and then the POST URL
http://localhost/api/Authentication/Subscribe
Hope this helps.