Annotation Interface Mutating


Declares that a REST operation changes state, so that binding it to an HTTP method defined as safe becomes a startup failure instead of a silent mistake.
The mistake this exists to catch

RFC 9110 defines GET, HEAD, OPTIONS and TRACE as safe: a client, a proxy, a crawler or a browser prefetcher may issue them freely, repeat them, and follow them without asking, precisely because they are defined not to change anything. Everything downstream of a request is built on that promise — caches store the response, link prefetchers fire the request before the user clicks, and every CSRF defence in existence, including LoopbackBoundary, applies its checks only to the methods that are not safe.

So a handler that mutates state behind GET does not merely misuse a verb. It sits behind whichever of those checks the application relies on, having quietly opted out of all of them — not by disabling anything, but by being classified as harmless. Nothing about it looks wrong at the call site, and nothing reports it at runtime, because from the boundary's point of view a GET arriving without a CSRF token is an ordinary read.

That is the failure mode this annotation exists to convert into a boot error:

// Fails at startup: this mutates, and @RestOp with no method= and a name that starts with none of // get/put/post/delete resolves to GET. @Mutating @RestOp public Result armRelease(...) {...}

The @RestOp case above is worth singling out, because the developer never typed GET anywhere: with no method= and a Java method name whose prefix is not one of get/put/post/ delete, the resolved method defaults to GET. A mutating operation therefore lands on a safe method through nobody's decision. The check resolves the method the same way dispatch does, so it sees the inferred GET rather than the absent annotation.

Why this is declared and not detected

The obvious alternative is for the framework to work out for itself which handlers mutate, and it was rejected because no sound version of it exists. "Mutates" is a claim about effects on state the application cares about, and that has no syntactic signature:

runner.run(List.of("svn", "commit", path)); // mutates the world runner.run(List.of("svn", "info", path)); // mutates nothing

Those are the same call to the same method with the same argument type, and telling them apart requires knowing what svn does. No analysis of the Java program can supply that. Any analysis strong enough to see through the indirection real applications use — an interface for the process runner, a supplier for the credential, a method reference for the callback, an HTTP client whose URL is chosen at runtime — is a whole-program points-to analysis, and it would still be wrong on the two lines above.

A naming heuristic (set*, save*, delete*) fails in both directions — getOrCreateSession mutates, applyFilter does not — and has a worse property than being unreliable: it would make renaming a Java method change the application's security posture.

Inspecting the signature fares no better. A mutating operation need take nothing but a @Path: deleting a record by id, or validating a stored credential and caching the verdict, both mutate while taking no body at all.

The decisive argument is about the cost of being wrong. This check's failure mode is the application does not start. A check that can refuse to start an application it merely suspects will be switched off, and a check that has been switched off protects nothing — including against the real contradictions it would have caught. So the rule here is one a machine can evaluate with certainty: the developer stated that this operation mutates, and the operation is bound to a method defined as safe. Those cannot both be intended, and no inference is involved in noticing it.

What this does not catch

An operation that mutates and says nothing is invisible to this check. The check finds contradictions, not omissions, and a developer who does not reach for the annotation gets no protection from it. That limit is inherent in the previous section: the alternative to a declaration that can be forgotten is an inference that can be wrong, and at a gate that stops the application from booting, wrong is worse.

What makes the limit acceptable is that the annotation is strictly additive. It can only ever cause a boot failure; it never relaxes a runtime check. Its opposite, @RestOp(read=true), is accepted only on custom methods and cannot declare a POST, PUT, PATCH or DELETE safe. An operation that omits it is therefore treated exactly as it would be if this annotation did not exist — the boundary still classifies by HTTP method, still treats an unknown method as state-changing, and still applies every write check to every non-safe method. Adopting the annotation can move an application from "wrong and silent" to "refuses to start"; it cannot move one from "protected" to "unprotected".

For the same reason this is method-level only. A class-level form meaning "everything here mutates" would be convenient and would immediately produce false failures on the page-rendering GET that nearly every such resource also has — reintroducing exactly the false-positive problem that ruled out inference.

When it runs

When the resource's operation table is assembled: at startup under Rest.eagerInit(), and otherwise on the first request to that resource. In both cases it runs before any operation on that resource can be dispatched, so a contradiction cannot be reached by a request. An application wanting the failure at boot literally should set eagerInit.

The check is unconditional and needs no wiring. It is not part of any filter, because HTTP method safety is not a property of a security filter — it is a property of the resource, which the filter then depends on. An application with no @Mutating anywhere is unaffected.

Example:

// Correct: declared mutating, bound to a method that is not safe. Boots, and the boundary's write // checks apply to it because POST is not in the safe set. @Mutating @RestPost("/{version}/arm") public ArmResult arm(@Path("version") String version, @Content ArmRequest body) {...}

See Also:
Since:
10.0.0
  • Optional Element Summary

    Optional Elements
    Modifier and Type
    Optional Element
    Description
    Optional note describing what this operation changes.
  • Element Details

    • value

      Optional note describing what this operation changes.

      Included in the startup failure message when this operation is bound to a safe method, so the error can say what is at stake rather than only which method is wrong. Has no other effect.

      Returns:
      The description of what this operation changes.
      Default:
      ""