CodeGym /Courses /Java Server /405 Method Not Allowed and Allow

405 Method Not Allowed and Allow

Java Server
Level 26, Lesson 2
Available

1. Path and method: 404 vs 405

It is very easy to confuse 404 and 405, because from a beginner’s point of view both situations look the same: “well, the request still did not get handled.” But in HTTP these are two different questions, and the server must answer them differently. Imagine that path is the apartment address, and method is an action: ring the doorbell, deliver a package, or knock down a wall with a hammer drill.

If the client arrives at an address that does not exist, that is 404 Not Found: “we do not know anyone at that address.” If the address exists but the action is not allowed, that is 405 Method Not Allowed: “the apartment exists, but you cannot use a hammer drill here; here is the list of allowed actions.” And this very list is expressed by the Allow header.

To make this fully concrete, let us take our ReadLater API. The path /api/v1/reading-list exists, and for it GET (fetch the list) and POST (create an item) are appropriate. If a client sends PUT /api/v1/reading-list, that is not “path not found”; it is “method not supported for this path,” which means 405, not 404.

2. Why 405 is useful for the client

At a teaching level, one could say: “What difference does it make? Let us return 404 and be done with it.” But in the real world, 405 is one of those small details that make an API predictable and easy to use. When a client sees 405, it understands: “I got the action wrong, not the address.” That greatly speeds up debugging, especially when requests are assembled programmatically rather than by hand in Postman.

There is another bonus: when you return 405 together with Allow, you are literally giving the client a hint about “how to do it right.” For a person, this is like a sign on a door: “Reception is open: Mon–Fri 10:00–18:00.” Even if you still do not let them in, you make the server’s behavior friendly and understandable, which is rare, but nice.

And now an important practical point from our day: 405 happens before body reading and validation. If the method is not allowed, there is no point in reading JSON, no need to call ObjectMapper, and no need to run the validator. Otherwise you can easily end up with a strange situation: the client sent the wrong method, and the server replied “JSON is broken” or “title is required.” Formally that may be true, but semantically it is an answer to the wrong question.

3. Allow and its relation to 405

Allow is an HTTP header that tells the client the list of methods allowed for a specific resource/path. In the context of our course, one very simple rule matters: if you return 405 Method Not Allowed, then you should almost always add Allow and list the methods that this path actually supports.

The discipline here is this: Allow refers to a specific path, not to the whole application. If /health supports only GET, then Allow should be GET. If /api/v1/reading-list/{id} supports GET, PUT, and DELETE, then Allow must contain exactly those methods, even if somewhere else in the application there is a PATCH for status.

In our API, we will also keep a unified error contract: even for 405 we return a JSON ErrorResponse, so the client does not have to write separate error parsers. Allow remains a “transport hint” in the headers — like a sign at the entrance, while JSON is a clear explanation in words.

A tiny piece of code that usually looks as boring as possible (and therefore as correct as possible) is this:

import java.util.Set;

String allowHeaderValue(Set<String> allowedMethods) {
    // In HTTP, methods in Allow are listed with a comma and a space: "GET, POST"
    return String.join(", ", allowedMethods);
}

There is no magic here: by the standard, methods are separated by commas. Yes, this is that rare case where “just join the strings” is exactly what you need.

4. ReadLater routes and methods

To answer 405 correctly, the server needs to know: “if the path is recognized, which methods are allowed for it?” In larger systems this is stored in the router/framework, but here we have plain Java and manual routing, so we do it explicitly and simply. For a teaching API, it is perfectly fine to have a small “truth table” right in the code.

Below is our target path map. Notice that we describe patterns of paths: {id} is “some single segment after reading-list/,” and it may turn out to be non-numeric. This matters: we want to distinguish 405 from 400, and 400 for an invalid id will be handled later.

Path (pattern) Allowed methods Short meaning
/health GET Check whether the server is alive
/api/v1/reading-list GET, POST List and creation
/api/v1/reading-list/{id} GET, PUT, DELETE Read/update/delete by id
/api/v1/reading-list/{id}/status PATCH Partial status update

With this table, you can mentally check several situations. For example, GET /api/v1/reading-list/1/status should be 405 (the path exists), while GET /api/v1/reading-list/1/status/extra should be 404 (that is already a different path).

5. allowedMethods(path): recognizing the path

Now comes an important “adult” nuance. We want to recognize the path in such a way that "/api/v1/reading-list/abc" still counts as an “item path,” otherwise we will never be able to return a correct 400 for a non-numeric id. Therefore, for routing by path we check the structure of segments, but do not validate their meaning (for example, whether id is a number). We will leave semantic checks to the “parsing/validation” step.

Below is an example of a very educational, but useful, resolver. Pay attention to the pattern [^/]+: this means “one segment that does not contain /.” In other words, “anything, as long as it is not empty and does not split into two segments.”

import java.util.Set;

Set<String> allowedMethods(String path) {
    // First, the simplest and most exact matches
    if ("/health".equals(path)) return Set.of("GET");
    if ("/api/v1/reading-list".equals(path)) return Set.of("GET", "POST");

    // "{id}" is exactly one segment, not necessarily a number (we will check the meaning later)
    if (path.matches("/api/v1/reading-list/[^/]+")) return Set.of("GET", "PUT", "DELETE");

    // The status endpoint is a separate route
    if (path.matches("/api/v1/reading-list/[^/]+/status")) return Set.of("PATCH");

    // The path is not recognized — that means "route not found" (later this becomes 404)
    return Set.of();
}

Why is this not a “homegrown framework”? Because we are not building a universal routing system, not adding annotations, not dragging in reflection, and not inventing a DSL. We are simply and honestly encoding four rules for four paths in our teaching API. That is exactly the level of simplicity that is still useful, not dangerous.

6. In handler: 404/405 before body and validation

The most important takeaway from the lecture is the order of checks. First we decide whether the path exists, then whether the method is allowed, and only then do we start the “expensive” operations: reading the body, ObjectMapper.readValue(...), DTO validation, and calling the service and repository. This order makes the API logically correct and avoids strange errors like “we return validation errors even though the method is not supported at all.”

Below is a compact fragment of what the beginning of handle(...) usually looks like in our ReadingListHttpHandler. You can already feel the “backend rhythm” here: quickly determine the transport conditions, and only if they are OK move on to the business steps.

import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.util.Set;

void handleRoutingGuard(HttpExchange exchange, String path) throws IOException {
    // Request method (GET/POST/PUT/...)
    String method = exchange.getRequestMethod();

    // List of allowed methods for a specific path (if empty, the path was not recognized)
    Set<String> allowed = allowedMethods(path);

    // First answer the question "does the route exist?"
    if (allowed.isEmpty()) { 
        writeNotFound(exchange); 
        return; 
    }

    // Then — "is the method allowed for this route?"
    if (!allowed.contains(method)) { 
        writeMethodNotAllowed(exchange, allowed); 
        return; 
    }

    // Important: at this point we still have not read the body or validated the DTO
}

Notice two things. First, we do not read exchange.getRequestBody() at all. Second, writeNotFound(...) and writeMethodNotAllowed(...) are exactly the places where you use the unified ErrorResponse from the previous lecture, and in the case of 405 you also add Allow.

And here is an example of writeMethodNotAllowed(...) in minimal form. It is short and not “smart,” but it does exactly what the contract requires.

import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.util.List;
import java.util.Set;

void writeMethodNotAllowed(HttpExchange exchange, Set<String> allowed) throws IOException {
    // Transport hint to the client: which methods are allowed for this path specifically
    exchange.getResponseHeaders().set("Allow", String.join(", ", allowed));

    // The error body, however, follows our unified JSON contract
    writeJson(exchange, 405, new ErrorResponse(
            "METHOD_NOT_ALLOWED", "The method is not supported for this path", List.of()
    ));
}

Here, writeJson(...) is your already existing utility from the previous days, which sets Content-Type: application/json; charset=utf-8, sends the status, and serializes the body. If in your project you centralized error mapping, you can build ErrorResponse not right here, but through a shared factory method; the idea itself does not change.

7. Checking 405 with Postman/curl

When you implement 405, it is very useful to do a quick manual check, because this is one of those cases where “the code compiles,” but the contract is easy to mix up. The main things you should see are: status 405, the Allow header, and a JSON error in a unified format.

Here is an example of a request that often happens by mistake: the client tries to make a POST to an item path because it mixed up “creation” and “update.”

# Wrong method for item endpoint
curl -i -X POST http://localhost:8080/api/v1/reading-list/1
# HTTP/1.1 405 Method Not Allowed
# Allow: GET, PUT, DELETE

If instead you see 404, it means you are checking method+path “all at once” and cannot distinguish an existing path from an invalid method. If you see 400 with a message about JSON, it means you are reading the body and validating the DTO before checking whether the method is allowed, which is semantically wrong.

And here is another illustrative example: GET on a status endpoint. It exists, but supports only PATCH.

# The endpoint exists, but GET is forbidden for it
curl -i http://localhost:8080/api/v1/reading-list/1/status
# HTTP/1.1 405 Method Not Allowed
# Allow: PATCH

And finally, an example where it should be exactly 404: the path does not look like any of our routes, even structurally.

# The route is not recognized even structurally — this is 404
curl -i http://localhost:8080/api/v1/reading-list/1/status/extra
# HTTP/1.1 404 Not Found

These three checks give you a very good feeling: you are not just “returning something,” you are actually answering the right question with the right status.

8. Typical mistakes when working with 405 and Allow

Mistake #1: returning 404 for an existing path with an unsupported method.
This almost always happens when routing is written in the style of “look for a method+path match all at once.” For example, you check if (method.equals("GET") && path.equals("/api/v1/reading-list")), and in the else branch you return 404. As a result, PUT /api/v1/reading-list turns into “route not found,” even though the route exists. The fix is not “add more ifs,” but changing the order of thinking: first recognize the path, then check the method.

Mistake #2: building Allow as “all methods in the application.”
This is a typical logic error: “well, we have GET/POST/PUT/PATCH/DELETE in general, so let us list them all.” But Allow must reflect only what is allowed for this path. If you list too many methods, that is not a hint but misinformation: the client will try PATCH on /api/v1/reading-list, get 405 again, and start doubting your sanity (and would be right).

Mistake #3: reading the body and validating the DTO before checking the method.
This is not just “extra work,” it is also a source of wrong responses. For example, the client sent a GET with a body (yes, that sometimes happens), you tried to parse JSON and returned 400 for a broken payload, although the correct answer would have been 405 (if GET is not supported for that path) or simply processing the GET without reading the body. In our course, the rule is simple: 404/405 checks must happen as early as possible.

Mistake #4: trying to determine path existence through the “validity of id.”
If you decide: “the route /reading-list/{id} exists only if {id} is a number,” then GET /api/v1/reading-list/abc becomes 404 instead of 400. That breaks the careful semantics we built on Day 24: a non-numeric id is a bad request (400), not a “disappeared route.” For this reason, path recognition must be structural (segments and constant parts of the path), and semantic checks must be a separate step.

Mistake #5: replying to 405 with plain text when everything else is JSON.
Technically, the server can return anything, but from the client’s perspective this makes life much harder: it has to parse errors in two formats. Our project goal is the exact opposite: all errors are ErrorResponse. Therefore, for 405 we also return JSON in a unified form, simply adding Allow as a transport hint.

1
Task
Java Server, level 26, lesson 2
Locked
`405 Method Not Allowed` for two simple paths
`405 Method Not Allowed` for two simple paths
1
Task
Java Server, level 26, lesson 2
Locked
Distinguish `404` and `405` on paths with `{id}` and `/status`
Distinguish `404` and `405` on paths with `{id}` and `/status`
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION