== Something to remember: HTML forms are anonymous
By and large, web programming frameworks have settled on a common
model of handling HTML forms. You have named (or typed) forms
with named and typed fields and you use the framework to render
them into HTML and extract them from _POST_ responses. [[Django
http://www.djangoproject.com/]] programmers, for example, have a
familiar, reflexive idiom:
> class MyForm(forms.Form):
> name = forms.CharField(...)
> ...
>
> def handle_my_url(request):
> if request.method == "POST":
> form = MyForm(request.POST)
> if form.is_valid():
> ...
This simple, clear approach is misleading. It's misleading because it
makes the whole process look sort of like storing objects, which means
that of course you're only going to get a valid _MyForm_ back from the
HTTP POST if you actually put one there in the first place (or the user
made up the POST data themselves to fool you, which you can ignore).
The thing is, ~~HTML forms are anonymous~~. In their natural state,
the only way you can tell different types of forms apart is by the URL
they are sent to and the names of the form fields that they have. There
is nowhere natural in a HTML form where you can say 'this is a MyForm
form'; in general, you have to infer that from the fact that it has
all the fields that MyForm has and is _POST_'d to a URL that expects a
MyForm form.
(Your web framework may be adding a hidden label field that it uses to
be sure, but you have to check the generated HTML in order to know for
sure.)
So suppose that you have two different forms with the same form fields;
this means that the only way to tell these forms apart is by the URL
that each of them uses. If they use the same URL (for example because
there are alternate versions of the page, with forms that have a
different meaning), you can't tell them apart at all. You can render a
page with a 'MyForm1', have the user POST it back, and happily retrieve
a 'MyForm2' from the POST response. Although these two forms looked like
they were distinct and different objects in your code, in HTML they are
actually the same thing.
(It's as if your programming language ignored the type of things when
doing 'is-a' and equivalence checks and only checked that two instances
had all of the same fields. There are languages that work this way; I
believe the term of art for it is 'structural equality'.)
All of this is abstract sounding, so let me give a concrete example
where I almost shot my foot off this way. [[Our account request system
../python/DjangoORMDesignPuzzleII]] allows privileged users to do two
very special operations to requests: if a request is marked as having
been either accepted or rejected, you can reset it to 'pending', and
if a request is pending you can immediately delete it. In both cases
you need to confirm that you really do want to do this by ticking
off a checkbox, and both operations are done from the same 'detailed
information about this request' URL; which option the page gives you
depends on the request's state.
So we can create two forms:
> class ReallyRevive(forms.Form):
> yes_really = forms.BooleanField(...)
>
> class ReallyDelete(forms.Form):
> yes_really = forms.BooleanField(...)
Then we write code that tries to get and validate a ReallyRevive form
from the _POST_ response if the request is not pending, and do the same
with a ReallyDelete form instead if the request is pending. And we have
just created a dangerous race.
Suppose that two privileged users are both trying to revive the same
request at the same time. Both see the page rendered with a ReallyRevive
form, both tick the checkbox, and both submit the form, one somewhat
after the other. In the first form submission, the code retrieves
a valid ReallyRevive form and sets the request back to the pending
state. In the second form submission, the code successfully retrieves
a valid ReallyDelete form from the POST response *despite the fact that
it is actually a ReallyRevive response*, and immediately deletes the
just-revived request. Oops.
(You can see this as a REST violation if you want to. My view is that
these things happen in practice so I should be aware of the bear traps
waiting in the underbrush.)
The solution is to give your forms different field names; here we would
have a ((really_revive)) checkbox in one form and a ((really_delete))
checkbox in the other.