Availability: Taking on new projects

2 min read

A filter named after one form will fire on another

Checkout rejected a name that was plainly filled in. The validation was correct, and it was running somewhere its author never intended.

Hook names in WordPress tell you where a hook was introduced. They do not tell you everywhere it runs, and the difference is where a certain kind of bug lives.

The symptom

Checkout refuses to complete, reporting that a required name is missing, while the customer is looking at that name in the box in front of them. Nothing in the markup is duplicated, no script is being blocked, and the field posts correctly if you inspect the request.

The mechanism

A registration form posts its fields under plain names. Checkout posts the same information under prefixed names, because on that screen it is billing information. A validation callback written for the registration form looks for the plain names.

The trap is that the filter it hangs on also runs when checkout creates a customer account. So the callback executes in a context where the fields it wants cannot exist, finds nothing, and correctly reports what it was told to report. Every component involved is behaving exactly as written.

Why it is hard to see

Neither file is wrong on its own. Read the validation code and it is reasonable. Read the checkout code and it is reasonable. The fault only exists in the combination, which is why reading the two obvious files rarely resolves this class of problem, and why the first three hypotheses are usually about the markup.

How to write it so it cannot happen

  • Establish which form is being submitted before validating anything. A hidden marker in the form, with a nonce as the fallback, is enough.
  • Never infer the context from whether a field is present. Absence is the exact condition you are testing for, so you cannot also use it to decide whether to test.
  • When something fires unexpectedly, log the hook and the full set of posted keys rather than the values you expected. The missing prefix is obvious the moment you see the real keys and invisible until then.

The same shape, elsewhere

A guard written for one API route protects that route only. The next release adds another route into the same handler, or another plugin calls it, and the guard silently covers a shrinking fraction of the traffic. Scope guards to the condition you actually care about, not to the one caller you happened to know about when you wrote it.

Describe the symptom. I will tell you what it usually means.

Every enquiry gets a reply within one working day.