Learn · HTML
How the HTML form action attribute works
action is the URL a form submits to. It looks trivial until a relative path resolves somewhere unexpected, a second button needs to post elsewhere, or the server receives fields you did not expect. This page walks through the resolution rules from the HTML spec and the exact request that comes out the other end.
How the URL resolves
The action is parsed relative to the document's base URL, the same way a link's href is. A <base href> element changes that base for every relative action on the page. If action is missing or empty, the spec uses the URL of the form's document, query string included, so the form posts back to the page it is on.
For a page at https://example.com/blog/post/:
The trailing-slash case is the classic bug: the same relative action works on /contact/ and breaks on /contact. Use root-relative or absolute URLs for actions in shared templates.
action="thanks"resolves tohttps://example.com/blog/post/thanks(relative to the directory, so the trailing slash on the page URL matters).action="/thanks"resolves tohttps://example.com/thanks.action="../thanks"resolves tohttps://example.com/blog/thanks.action="https://forms.example.net/f/42"is used as written; the form navigates to another origin.- No
action, oraction="", posts tohttps://example.com/blog/post/itself.
Buttons can override the form
A submit button can override five form attributes for the submissions it triggers: formaction, formmethod, formenctype, formnovalidate and formtarget. Only the button that was used counts: the spec skips every button that is not the submitter when building the data, so a named button's name=value pair tells the server which one was pressed.
html
<form action="/posts" method="post">
<input name="title" required>
<textarea name="body"></textarea>
<!-- Publish: validated, posts to /posts, sends intent=publish -->
<button name="intent" value="publish">Publish</button>
<!-- Draft: skips validation, posts elsewhere, sends intent=draft -->
<button name="intent" value="draft"
formaction="/drafts" formnovalidate>Save draft</button>
</form>method and enctype together
enctype only applies to POST. A GET submission always URL-encodes the fields into the query string and, as the POST vs GET article covers, replaces any query already in the action. For POST the three values are:
method="dialog" is the odd one out: inside a <dialog> it closes the dialog and sets its returnValue from the submit button instead of submitting anything.
application/x-www-form-urlencoded, the default:name=Ada+Lovelace&topic=sales.multipart/form-data: required for file inputs; each field is a separate part. See multipart/form-data uploads.text/plain:name=valuelines with no escaping, so values containing=or newlines are ambiguous. It exists for debugging; do not parse it.
What the server receives
A cross-origin URL-encoded POST from a page on example.com looks like this on the wire:
http
POST /f/42 HTTP/1.1
Host: forms.example.net
Origin: https://example.com
Content-Type: application/x-www-form-urlencoded
name=Ada+Lovelace&email=ada%40example.com&topic=sales&topic=support&message=Line+one%0D%0ALine+twoThings that surprise people reading that body:
- Fields without a
nameare not sent at all. Neither are disabled fields, unchecked checkboxes and radios, or buttons other than the submitter. - Repeated names (
topicabove) arrive as repeated pairs. Whether your server keeps the first, the last or all of them is a property of its parser, not of HTML. - Newlines in values are normalized to CRLF during submission, so a textarea that reports 500 characters in JavaScript can arrive longer if it contains line breaks.
- The browser sends an
Originheader on POST, cross-origin or not. That is what a server-side origin check reads.
After the POST: answer with a redirect
A form submission is a navigation. Whatever the action URL returns is what the user sees, on whichever origin the action points at. If the action is a third-party endpoint, it has to redirect the browser back to a page on your site, or the user ends up looking at someone else's domain.
Use 303 See Other to a GET URL (Post/Redirect/Get) so reloading the result page does not resubmit. If you would rather keep the user on the page, intercept the submit and send it with fetch() instead.
How this works with Formward
With Formward the action is your form's endpoint, https://forms.formward.eu/f/<FORM_ID>, posted cross-origin from your page. After a successful classic submission the endpoint redirects with a 302 to the thank-you URL configured on the form, or to a _redirect value sent with the form.
A relative _redirect such as /thanks is resolved against the page that submitted the form (its Origin, falling back to Referer), so it lands on your site rather than on the endpoint's host. An absolute _redirect is only honoured when its origin is in the form's allowed origins, which keeps the endpoint from being used as an open redirect. The special fields docs list _redirect and the other underscore fields.
Sources
Point a form at an EU endpoint
Formward receives the POST, filters spam and stores submissions in Sweden. No server to run.