A proposal for adding PUT, PATCH, and DELETE support to HTML forms.
Proposal 1/3 in the Triptych Proposals.
PUT, PATCH, and DELETE support in forms should:
New values for the form method
attribute:
PUT
- makes the form issue a PUT request
PATCH
- makes the form issue a PATCH request
DELETE
- makes the form issue a DELETE request
All new method keywords are case insensitive.
Existing form controls (e.g. action
, enctype
) should operate identically.
All examples in this section assume that the host origin is https://example.com.
First, the user makes a reservation using a traditional POST form:selects a proper URI on behalf of the client.
This is the simplest RESTful pattern, and has the additional benefit of using a non-idempotent method, so the browser can guard against creating two new things when only one is desired.
More benefits to using this pattern will be discussed in the justifications section.
<form action="/reservations" method="POST"> <input type="text" name="name"> <input type="date" name="check-in"> <input type="date" name="check-out"> <input type="checkbox" name="has-pets"> <button>Submit</button> </form>The browser will send the following HTTP request:
POST /reservations HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded ... name=Alex%20Petros&check-in=2024-12-01&check-out=2024-12-02&has-pets=onAnd the server responds with a redirect to the newly-created reservation resource:
HTTP/1.1 303 SEE OTHER Location: /reservations/123
At the reservation page, the user is presented with two forms. The first one allows them to adjust their reservation:
<form action="/reservations/123" method="PUT"> <input type="text" name="name" value="Alex Petros"> <input type="date" name="check-in" value="2024-12-01"> <input type="date" name="check-out" value="2024-12-02"> <input type="checkbox" name="has-pets" checked> <button>Submit</button> </form>This new form offers the same controls as the POST form, but with some key differences:
/reservations
to /reservations/123
POST
to PUT
If the user changes the check-out date to 2024-12-03
, the browser will send the
following request:
PUT /reservations/123 HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded ... name=Alex%20Petros&check-in=2024-12-01&check-out=2024-12-03&has-pets=onThe server can then choose to either send back a new page or redirect to one.
PUT as a form method makes no sense, you wouldn't want to PUT a form payload. DELETE only makes sense if there is no payload, so it doesn't make much sense with forms either.
<form action="/reservations/123" method="DELETE"> <button>Delete Reservation</button> </form>Clicking "Delete Reservation" would result in the following HTTP request:
DELETE /reservations/123 HTTP/1.1 Host: example.comAnd the server could choose to either send a delete confirmation page, or redirect elsewhere.
Browsers should identically render the response content for all non-redirection codes; we do not propose any special behavior for codes like 201 (Created) or or 500 (Internal Server Error). This aligns with current browser behavior.
For PUT, PATCH, and DELETE requests, if the server responds with a 301, 302, or 307 status code, the browser should perform a subsequent request with the same HTTP method;
if the server responds with a 303 status code, the browser should perform a subsequent request with the GET method.One thing I forgot earlier, and which was the reason why I actually wanted PUT and DELETE
temporarily (!) on hold is redirect handling. The experimental Firefox implementation was copying
the redirect handling for POST (with respect to method rewriting), and it would have been bad to let this get into the deployed platform.
The experimental Firefox implementation was copying the redirect.
So it would be good to clarify that PUT and DELETE, when being redirected by 301/302 should *not* be rewritten to GET.
If the response redirects to a server of the same origin, the browser should directly issue the request.
For behavior when the response redirects to a server of a different origin, see
PUT and PATCH forms should send identical content to their POST equivalents.body
property of a fetch
request.
is highlighted by the different intent for the enclosed representation,
which implies that they support the same set of representations. Per
RF5789, which defines PATCH, the
difference between the PUT and PATCH requests is reflected in the way the server processes the
enclosed entity to modify the resource
, which again implies that they are capable of
representing resources the same way.
DELETE forms should format their content as URL parameters, like GET forms. While both GET and
DELETE body semantics are technically undefined, including content in the request body is somewhat
discouraged by the spec.DELETE request has no generally defined semantics,
it also states that a client SHOULD NOT
generate content in a DELETE request unless it is made directly to an origin server that has
previously indicated, in or out of band, that such a request has a purpose and will be adequately
supported
. While one could certainly argue that the server returning HTML with <form
method=DELETE>
constitutes indication of support for content, we defer to the library
ecosystem, which generally understands the similar language in the GET and DELETE specs as a tacit
discouragement of body content for both.
method=DELETE
should encode their inputs as part the URI,
emulating the behavior of forms with no method
, or method=GET
.
PUT, PATCH, and DELETE requests are unsafe (not read-only), and therefore can never be cached.
PATCH responses are technically cacheable in the same way that POST responses are: if
certain information is explicitly provided, subsequent GET requests may use the cached PATCH
response to represent that resource.
PUT and DELETE responses are never cacheable.
For same-origin requests, the browser should directly issue the request. For cross-origin requests, the browser should do the following:
action
attribute.
The behavior where the browser issues additional CORS requests if the server responds with a redirect is chosen to match the existing behavior in the fetch spec.
for requests that are more involved than what is possible with HTML's form element, a CORS-preflight request is performed, to ensure request’s current URL supports the CORS protocol.
Applying CORS to navigation was an implementation blocker for quite some time, as no precedent previously existed for this behavior.We cannot bypass the same-origin policy and enforcing CORS is theoretically possible, but would require integration of that to some extent with navigation, which is completely new ground.
It's possible that new, navigation-specific CORS headers could be added as well.
For PATCH requests, which are not idempotent, the user agent should behave as it does currently, asking the user for confirmation and warning that it may cause the data to be re-submitted.
For PUT and DELETE requests, which are idempotent, the user agent should resubmit the request. This allows servers to take advantage of the method semantics and create forms that users on unreliable connections can feel confident re-submitting.
First and foremost, new browser features must not expose existing servers or users to new vulnerabilities, so adding new CORS-safelisted methods or headers is out of the question. Fortunately, there is no need to do so.
PUT, PATCH, and DELETE forms only make available to HTML a highly useful subset of what is already available to the web page, via JavaScript.
Nothing is proposed that can't be accomplished currently with fetch
and FormData
.
Technically, this does increase the capabilities of clients that that have scripting disabled. The overwhelming majority of browser users, however, especially the ones most vulnerable to malicious websites, have scripting enabled. Servers obviously cannot build webpages that are only secure for users with scripting disabled, so this does not change the security profile of the server.
The only HTML control that this proposal alters is the form's method
attribute.
Because the only two supported methods, GET and POST, have such different purposes, it is highly unlikely that authors are setting this attribute dynamically; it is even less likely that authors are setting it dynamically with un-escaped user-generated input, and relying on the browser's incomplete implementation of HTTP methods to protect against XSRF.
The spec has never guaranteed that these would be the only two methods, and it's hard to imagine a practical use that would lead an author to that implementation.
The addition of CORS-restricted methods to HTML forms provides a massive opportunity to move developers onto a more secure pattern web applications.
PUT, PATCH, and DELETE requests are more secure, by default, than POST requests, because they will never be issued to a cross-origin server unless that server explicitly permits them.this action isn't worth standardizing
method, and work for legacy applications.
In this manner, CORS restrictions can be leveraged to deepen the web security pit of success.
Sites that make no cross-origin requests are easier to secure than sites that do.There are many ways to prevent CSRF, such as implementing [sic] RESTful API, adding secure tokens, etc.
Unfortunately, it doesn't describe how a RESTful API might mitigate CSRF.
What they likely mean by this is that ensuring GET requests do not have side effects mitigates a number of CSRF pathways, but it's also true that HTML does not does not properly support RESTful APIs, nor does it currently have the ability to make any unsafe HTTP requests that are fully protected by CORS.
For more on the importance of supporting REST and the ways in which HTML support is inadequate, see
so much coupling on display that it should be given an X rating.
Despite the misconceptions, REST remains the most powerful conceptual tool for building durable hypermedia applications. In this section, we make the case for how better method support in HTML can drive adoption of REST priciples and dramatically improve the median web application as a result.
In the dissertation that defines REST, Roy Fielding includes HTTP methods among the core interface
constraints of REST—specifically the constraint that messages need to be self-describing.REST enables intermediate processing by constraining messages to be self-descriptive:
interaction is stateless between requests, standard methods and media types are used to indicate
semantics and exchange information, and responses explicitly indicate cacheability.
Search my dissertation and you won't find any mention of CRUD or POST.
The only mention of PUT is in regard to HTTP's lack of write-back caching.
The main reason for my lack of specificity is because the methods defined by HTTP are part of the Web's architecture definition, not the REST architectural style.
Fielding actually appears to be somewhat ambivalent about whether the user agent should have any understanding of method semantics. In 2008, he writes:
That GET, POST, and PUT have semantic meaning outside the context of the application is a secondary concern; the primary obligation of the client is to allow the hypermedia API to describe itself, and then faithfully execute that description.You don't get to decide what POST means — that is decided by the resource. Its purpose is supposed to be described in the same context in which you found the URI that you are posting to."REST APIs must be hypertext-driven", Comment #13
method definitions (aside from the retrieval:resource duality of GET) simply don't matter to the REST architectural style. He also asks:
why shouldn't you use POST to perform an update? Hypertext can tell the client which method to use when the action being taken is unsafe.
when it is used in a situation some other method is ideally suited,including
complete replacement of a representation (PUT). So it's fine to use POST to update a resource, unless your update is a complete representation of that resource, in which case it's not? Is it fine to use POST method to delete a resource, a task for which DELETE is ideally suited?
A REST API should not contain any changes to the communication protocols aside from filling-out or fixing the details of underspecified bits of standard protocols, such as HTTP's PATCH method or Link header field. Workarounds for broken implementations (such as those browsers stupid enough to believe that HTML defines HTTP's method set) should be defined separately, or at least in appendices, with an expectation that the workaround will eventually be obsolete.Exactly what he means by
browsers stupid enough to believe that HTML defines HTTP's method setis a little vague, but it clearly demonstrates frustration with HTML's limited method support.
We do not propose that HTML execute arbitrary HTTP methods specified in the form's method attribute, although that would fit nicely within the REST guidelines by allowing web APIs to freely self-describe across an already-available dimension.
HTTP methods have become more central to the developer community's conception of REST than
Fielding perhaps intended, and are one of its better-understood concepts.
In Jeremy Richardson's Maturity Heuristic, later dubbed the
"Richardson Maturity
Model" by Martin Fowler, proper use of HTTP methods is one of the three levels that determine
how well an application adheres to REST principles.
The increased salience of HTTP methods to REST is not a perversion of the concept, but a practical evolution of it, born from real-world use. Aspirational REST adherents have discovered that it is much easier to uphold a consistent representation of a resource (via URIs) when you have a standardized semantic to describe how the enclosed resource is to be modified.
For instance, this overview is from the first API tutorial in the latest ASP.NET Core documentation:
API | Description | Request body | Response body |
---|---|---|---|
GET /api/todoitems |
Get all to-do items | None | Array of to-do items |
GET /api/todoitems/{id} |
Get an item by ID | None | To-do item |
POST /api/todoitems |
Add a new item | To-do item | To-do item |
PUT /api/todoitems/{id} |
Update an existing item | To-do item | None |
DELETE /api/todoitems/{id} |
Delete an item | None | None |
This how most developers understand REST: a service is RESTful if it uses methods and URIs to describe what action you're taking on what resource. But if you look closely, you'll notice that it's not REST: the API returns JSON data, not hypermedia. The popular conception of REST is stuck at Level 2 of the Richardson Maturity Model.
Developers choose to build APIs with the standardized method grammar—in spite of missing HTML support—because it's simpler.
An API that supports PUT /users/123
and DELETE /users/123
is easier to
describe and code than a POST /users/123
API whose body semantics alter how it processes the enclosed resource.
The usefulness of methods as an HTTP semantic—a priori to the semantics of the methods
themselves—is so self-evident that the hypertext transfer protocol has long standardized a bunch of
additional methods; all that remains is for the dominant hypertext markup to support them.
REST is an enduring paradigm that suites a wide variety of web applications, and developers today have a number of good libraries to choose from if they wish to implement it. But even a client-loaded library with the perfect interface can never replace the functionality or durability of an official implementation.
Most libraries that implement REST primitives use them with partial page replacement.
This is largely due to demand in the developer ecosystem for partial page replacement, but it masks an important limitation: JavaScript cannot modify browser navigation primitives.
Given HTML's tremendous backwards- and forwards-compatibility guarantees, its capabilities guide the design of durable interactive applications.
For instance, the vast majority of Wikipedia's functionality can be described with hypertext primitives—including its relatively limited interactivity.method=DELETE
might be helpful for something like deleting a comment on a user talk page, but by and large, Wikipedia's core functionality does not involve a lot of deleting things, so the compromises involved with representing the deletions it does has are minimal.
Many applications that thrive on the web have more complicated resource lifecycles than Wikipedia, like banking, travel bookings, and social media. The new lifecycle methods would make it possible for those applications to built their interactivity in a hypertext-driven style, and take full advantage of the browser's reliability, security, and longevity as an application platform.
The most common way that developers compensate for the lack of proper HTTP method support is to
include a hidden input that overrides the method.
<input name=_method value=PUT>
input.
<form method="post" action="/users/123"...> <input type="hidden" name="_method" value="put"> ... </form> <form method="post" action="/users/123"...> <input type="hidden" name="_method" value="delete"> ... </form>
This would be a Level 1 on the Richardson Maturity Model. The URIs consistently identify a
resource (user/123
), but the method is always POST, so the verbs aren't in use.
This pattern has a number of drawbacks that would be rectified by proper PUT, PATCH, and DELETE
support:
Another workaround is to encode the method semantics straight into the URI.
<form method="post" action="/users/123/put"...> ... </form> <form method="post" action="/users/123/delete"...> ... </form>
This actually resolves some of the operational issues with the hidden input hack. The different actions are visible to the transport layer (although in a slightly harder-to-parse location than the proper method field), and server routers can easily declare separate handlers for each action.
But it is certainly not REST. In fact, it regresses on the Richardson Maturity Model from even the hidden input hack, all the way back down to 0.
Where the hidden input at least used URIs to identify resources, now the URIs don't even represent resources anymore;
they represent a combination of resource and method, mixing the semantics of both.
The problem immediately becomes apparent when you try to add additional sub-resources after /users/123
:
sometimes what comes after the 123
is an action, and sometimes it's a sub-resource.
This is a hassle to code, and it's a hassle to understand.
The overall impact is to unmoor the application from any universal semantics.
The standardized HTTP methods guide the developer to a clear and consistent pattern.
If the developer is not presented with a consistent set of common verbs for common tasks, they are liable to invent their own.
Why shouldn't /put
be /create
, or /delete
be /remove
?
The obvious smell of this pattern, when placed next to the actual HTTP methods, leads developers to conclude, correctly, that the missing methods limitation is endogenous to HTML, and the solution is to augment or abandon HTML rather than throw out URI semantics along with them.
Support for all HTTP methods is widespread in server side frameworks. Currently, this support is
mainly used for JSON-based APIs, since JavaScript-based network interactions via technologies like
XmlHttpRequest (xhr)
or fetch()
allow
JavaScript developers to access these HTTP methods.
Below is a table of some major server side frameworks in various programming langauges, and their support for HTTP methods.
Language | Framework | HTTP Method Support |
---|---|---|
JavaScript | Express | All HTTP Methods |
Next.js | All HTTP Methods | |
Astro | All HTTP Methods | |
Python | Flask | All HTTP Methods |
Django | All HTTP Methods | |
.NET | ASP.NET | All HTTP Methods |
Java | Spring Boot | All HTTP Methods |
Javalin | All HTTP Methods | |
Go | Core HTTP Library | All HTTP Methods |
PHP | Laravel | All HTTP Methods |
Support for all HTTP methods is also widespread in client-side frameworks. Interestingly, this support is increasingly used for HTML-based APIs, in additional to JSON-based APIs, indicating that there is demand for a full implementation of HTTP methods in HTML.
Below is a table of client side frameworks that use HTML as a network format, as well as their support for the various HTTP.
Framework | HTTP Method Support |
---|---|
htmx | All HTTP Methods |
Unpoly | All HTTP Methods |
Alpine-Ajax | All HTTP Methods |
pjax | All HTTP Methods |
Hotwire Turbo | All HTTP Methods |
While the above table establishes the general support for the full gamut of HTTP methods, it does not establish the
usefulness of them for web developers. In order to get a feel for that, we can search Github for use of the following
htmx attributes: hx-put
, hx-patch
& hx-delete
, which are used to issue
the HTTP PUT
, PATCH
and DELETE
methods respectively.
Below is a table of the results of these searches:
Attribute | Count | % of hx-get |
% of hx-post |
---|---|---|---|
hx-get
|
35.2k | 100% | - |
hx-post
|
22.5k | 64% | 100% |
hx-put
|
3.8k | 11% | 17% |
hx-patch
|
1.2k | 3% | 5% |
hx-delete
|
6.5k | 18% | 29% |
You can see that there is widespread use of the three additional methods in htmx-based applications, particularly
hx-put
and hx-delete
. It is worth noting the popularity of the DELETE
method
in HTML-based web applications. This is because it allows web developers to issue two different methods to the same
URL. A web developer can use POST
to /reservations/
to create a new reservation and
a POST
to /reservations/123
to update an existing reservation (even if they would prefer to
issue a PUT
or PATCH
) but must create a separate end-point (or use another workaround) to
delete that reservation.
With the addition of the DELETE
method, web developers can follow the natural, resource-oriented URL
pattern.
One common web application pattern that is not well-supported in HTML due to the lack of additional HTTP methods beyond GET and POST is a logout flow. It is common in many web applications to have some sort of login functionality, creating a session for a user where their identity is established for future requests. Once a user has logged in, web applications typically allow users to then choose to log out of the web application. This is typically done in one of two ways:
GET
to a path (e.g. /session
) to log them out.
POST
to a path (e.g. /session
) to log them out.
The ideal request for this common piece of functionality would be one that is known to be mutative, known to be idempotent, and accurately describes the action being taken.
If HTML were able to issue a DELETE
to /session
it would these needs in a way that it currently cannot, making implementing this extremely common functionality less error-prone and more natural.
The most popular web frameworks all support the ability to declare handlers for HTTP route and method combos. Access to additional methods dramatically simplifies route declaration and remove the potential for footguns.
In this example, we'll be using ExpressJS (a popular JavaScript server), and re-using the hotel reservation concept from
router.post( '/reservation', requireLogin, createReservation) router.get( '/reservation/:reservationId', requireOwnership, getReservation) router.post( '/reservation/:reservationId', requireOwnership, updateReservation)
Even if you are unfamiliar with ExpressJS, it is relatively easy to understand what is going on here.
ExpressJS lets you declare a method, a route, and then a series of functions that handle the request.
To make a reservation, the client issues a POST request to /reservation
, then runs the requireLogin
function, and if that succeeds, runs the createReservation
function.requireOwnership
method.
How then, should we implement the ability to delete a reservation? Without access to additional HTML methods in the form, we have two choices. We can double-up on a handler:
// Note the new updateOrDeleteReservation method router.post( '/reservation', requireLogin, createReservation) router.get( '/reservation/:reservationId', requireOwnership, getReservation) router.post( '/reservation/:reservationId', requireOwnership, updateOrDeleteReservation)
No longer does the router, and therefore the network layer, have a complete view of the application's functionality, because deletes and updates happen within the same function.
If POST /reservations/:reservationId
starts throwing internal server errors, it won't be immediately obvious what functionality is impacted.
Also, deletes and updates might have different permissions associated with them! What if you want to implement group reservations, and give everyone in the group permission to edit the reservation, but only the owner permission to delete it? The safest, simplest, and most secure way would be to have separate routes with separate permission functions, but since we're re-using one function for two actions, we don't have access to that. To accomplish that, we have to mess with the URI:
router.post( '/reservation', requireLogin, createReservation) router.get( '/reservation/:reservationId', requireGroupMembership, getReservation) router.post( '/reservation/:reservationId', requireGroupMembership, updateReservation) router.post( '/reservation/:reservationId/delete', requireOwnership, deleteReservation)
Now we have the ability to declare separate permissions, but we've lost the essential semantic that the URI represents a resource.
This get messy fast.
What if reservations have sub-resources, like members?
It's easy to model that for getting and updating the reservation, because you just add /members
to the URI.
But now we have two confusing cases—one where the sub-resource after the reservation represents a new thing, and one where it represents an action on the main resource.
This does not scale.
The ideal situation is obvious, from the server's standpoint:
router.post( '/reservation', requireLogin, createReservation) router.get( '/reservation/:reservationId', requireGroupMembership, getReservation) router.put( '/reservation/:reservationId', requireGroupMembership, updateReservation) router.delete( '/reservation/:reservationId', requireOwnership, deleteReservation)
Each action has a distinct method, permission, and handler. The client has access to idempotency semantics now, so the client knows that it's safe to retry the PUT and DELETE requests. The network layers can log and track each of the actions at an appropriate level of granularity. And most importantly, the purpose of the server is clear and legible to present and future maintainers.
In addition to adding PUT, PATCH, and DELETE support to the method
attribute, it makes a lot of sense to add another attribute, custommethod
, that overrides the value in method
:
<form action="/reservations/123" method="POST" custommethod="PUT"> <input type="text" name="name"> <button>Submit</button> </form>
In this example, browsers that support PUT and custommethod
would issue a PUT request to the specified action, while browsers that do not support those features would issue a POST request (servers would have to support both methods, of course).
This solves the problem where existing browsers that do not recognize the value in method
will fallback to a GET request, by allowing method
to serve as a "best supported method" fallback, while custommethod
explicitly denotes experimental behavior.
Thanks to @jlunman for asking us to address this issue.
Another major advantage of the custommethod
attribute is that it allows for a more robust polyfill mechanism.
The existing Triptych polyfill has the crucial caveat that client-side JavaScript cannot modify navigation in the manner necessary to create the robust experience proposed here.
The addition of a bridge attribute enables a better, navigation-only polyfill, from the server side:
<form action="/reservations/123" method="POST" custommethod="PUT"> <input type="hidden" name="_method" value="PUT"> <input type="text" name="name"> <button>Submit</button> </form>
While a bit clunky visually, this form builds on the existing method workarounds to create a smooth upgrade path.
Servers that recognize the method=PUT
.
We chose custommethod
as the attribute name in anticipating that it will one day be used for proper custom HTTP method support, of the kind anticipated in this working group note.
Eventually, it can be used for entirely arbitrary HTTP methods, while method
is reserved for officially supported ones.
In this manner, the upgrade/fallback mechanism proposed here can be re-used for future additions to HTTP's methods.
PUT and DELETE are necessary to include a full CRUD grammar in HTML; PATCH is not.
If you are going to do the work to generalize the <form>
method attribute anyway, it doesn't seem like there's huge benefit to omitting PATCH. Nevertheless, the proposal could mostly succeed in its goals without PATCH, since most RESTful design practices focus on PUT and DELETE.
There's no inherent reason why a DELETE request couldn't send body content, and many popular frameworks, like ExpressJS, do support it. Nor is DELETE content expressly prohibited by the spec, which allows for such requests if the origin server has indicated that it supports them. Since most HTML forms are same-origin, it could make sense to allow the form to indicate that it would be fine with DELETE content.
A usebody
attribute could be included to indicate support.
If present, the form would send its data as part of the body, for DELETE (or GET) requests;
it would be ignored for all other methods.
overridemethod
to custommethod