Other 4xx codes
All HTTP status codes in ASP.NET Core
4xx Client error
The server will not process the request because of a client error: malformed syntax, invalid framing or, in APIs, invalid input.
In ASP.NET Core: The standard answer to validation errors. [ApiController] sends a ValidationProblemDetails body automatically.
The StatusCodes constant, HttpStatusCode enum name, reason phrase, class, and how EnsureSuccessStatusCode() and the standard resilience handler treat 400.
| Constant | StatusCodes.Status400BadRequest |
|---|---|
| HttpStatusCode | HttpStatusCode.BadRequest |
| Reason phrase | Kestrel sends Bad Request |
| Class | 4xx, client error |
| IsSuccessStatusCode | false |
| EnsureSuccessStatusCode() | throws HttpRequestException: Response status code does not indicate success: 400 (Bad Request). |
| Standard resilience handler | does not retry it |
TypedResults.BadRequest()
TypedResults.BadRequest<TValue>(error)
Results.StatusCode(StatusCodes.Status400BadRequest)
Results.Problem(statusCode: StatusCodes.Status400BadRequest, detail: "...")BadRequest()
BadRequest(error)
ValidationProblem(modelStateDictionary)
ValidationProblem()
ValidationProblem(detail, instance, statusCode, title, type, modelStateDictionary)
StatusCode(StatusCodes.Status400BadRequest)
Problem(statusCode: StatusCodes.Status400BadRequest, detail: "...")What Results.Problem(statusCode: 400) sends (with AddProblemDetails()), as application/problem+json:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Bad Request",
"status": 400,
"traceId": "0HNA1B2C3D4E5:00000001"
}A controller's Problem(statusCode: 400) sends the same body.
A bare StatusCode(400) in an [ApiController] also gets a ProblemDetails body automatically; Results.StatusCode(400) in a minimal API sends an empty body unless you add UseStatusCodePages().
Setup
app.Map("/ws", async (HttpContext ctx) =>
{
if (!ctx.WebSockets.IsWebSocketRequest) { ctx.Response.StatusCode = 400; return; }
...
});Request
GET /ws HTTP/1.1Response (recorded)
HTTP/1.1 400 Bad RequestBrowsers and HttpClient cannot open a WebSocket with a normal request; the common pattern answers 400.
Setup
app.MapPost("/minimal", (Payload p) => p.Name);Request
POST /minimal HTTP/1.1
Content-Type: application/json
{"name":Response (recorded)
HTTP/1.1 400 Bad RequestThe JSON cannot be read, so minimal APIs answer 400. In Production the body is empty, so the client does not learn what was wrong.
Fix: Add builder.Services.AddProblemDetails() and app.UseStatusCodePages() for a ProblemDetails body.
Setup
app.MapPost("/minimal", (Payload p) => p.Name);Request
POST /minimal HTTP/1.1
Content-Type: application/json
Content-Length: 0Response (recorded)
HTTP/1.1 400 Bad RequestA required body parameter with no body is a 400. Declare the parameter nullable (Payload?) if the body is optional.
Setup
app.MapGet("/page", (int size) => size);Request
GET /page?size=abc HTTP/1.1Response (recorded)
HTTP/1.1 400 Bad Requestsize cannot be parsed as int, so the request fails binding with 400 and an empty body.
Setup
public class Thing { [Required] public string? Name { get; set; } }
[HttpPost]
public IActionResult Create(Thing t) => Ok(t);Request
POST /api/things HTTP/1.1
Content-Type: application/json
{}Response (recorded)
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Name": [
"The Name field is required."
]
},
"traceId": "0HNA1B2C3D4E5:00000001"
}[ApiController] checks ModelState before the action runs and answers with ValidationProblemDetails: an errors object keyed by property.
Setup
[HttpPost]
public IActionResult Create(Thing t) => Ok(t);Request
POST /api/things HTTP/1.1
Content-Type: application/json
{"name":Response (recorded)
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"t": [
"The t field is required."
],
"$.name": [
"Expected depth to be zero at the end of the JSON payload. There is an open JSON object or array that should be closed. Path: $.name | LineNumber: 0 | BytePositionInLine: 8."
]
},
"traceId": "0HNA1B2C3D4E5:00000001"
}The errors show the JSON reader's message under a JSON path ("$.name"), plus a second error keyed by the action's parameter name ("t"). Clients see your parameter names, so name them for the API.
Setup
// any Kestrel appRequest
GET /items HTTP/1.1
(no Host header)Response (recorded)
HTTP/1.1 400 Bad RequestKestrel rejects HTTP/1.1 requests without Host with 400 before your code runs, as HTTP/1.1 requires.
Setup
// any Kestrel appRequest
GET /it ems HTTP/1.1Response (recorded)
HTTP/1.1 400 Bad RequestAn unencoded space in the path breaks the request line; Kestrel answers 400.
Setup
// appsettings.json
{ "AllowedHosts": "example.com" }Request
GET / HTTP/1.1
Host: localhost:5000Response (recorded)
HTTP/1.1 400 Bad Request
Content-Type: text/html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd">
<HTML><HEAD><TITLE>Bad Request</TITLE>
<META HTTP-EQUIV="Content-Type" Content="text/html; charset=us-ascii"></ HEAD >
<BODY><h2>Bad Request - Invalid Hostname</h2>
<hr><p>HTTP Error 400. The request hostname is invalid.</p>
</BODY></HTML>WebApplication.CreateBuilder adds host filtering. A Host not listed in AllowedHosts gets a 400 with a small HTML page, "Bad Request - Invalid Hostname", before routing runs.
Fix: List every host name the app is reached by, including the one your reverse proxy forwards.
Setup
public record Invalid([property: Required] string Name);
[HttpPost]
public IActionResult Create(Invalid r) => Ok(r);Request
POST /api/records HTTP/1.1
Content-Type: application/json
{}Response (recorded)
HTTP/1.1 500 Internal Server Error
InvalidOperationException: Record type 'RecordsController+Invalid' has validation metadata defined on property 'Name' that will be ignored. 'Name' is a parameter in the record primary constructor and validation metadata must be associated with the constructor parameter.MVC refuses validation attributes on the properties of a positional record and throws for every request, so the endpoint always answers 500 (the body shows the exception, caught for this test).
Fix: Put the attribute on the parameter: public record Invalid([Required] string Name); then invalid input gets a normal 400.
Setup
app.MapGet("/items/{id:int}", (int id) => id);Request
GET /items/abc HTTP/1.1Response (recorded)
HTTP/1.1 404 Not FoundRoute constraints decide whether the route matches at all. "abc" is not an int, so there is no match and the answer is 404, not 400.
Fix: Use constraints to choose between routes, not to validate input. Without :int, "abc" fails binding and gives 400.
Every response on this page was recorded from ASP.NET Core 10.0.12 on Kestrel in the Production environment; the HttpClient rows come from .NET 10.0.12 and Microsoft.Extensions.Http.Resilience 10.10.0.
All HTTP status codes in ASP.NET Core