Instruct htmx that HTTP 422 is not a “fatal error”

I use HTTP 422 to signal that a form was submitted with bad data,
which i believe is the correct status code: “indicates that the server
understands the content type of the request content […], and the syntax
of the request content is correct, but it was unable to process the
contained instructions.”[0]

htmx, however, treats all 4xx status codes as error and, by default,
does not swap the target with the response’s content.  Until i found out
that i could change that behaviour, i worked around this limitation by
returning HTTP 200 for htmx requests, but it is a waste of time given
that htmx _can_ accept HTTP 422 as a non-error.

[0]: https://www.rfc-editor.org/rfc/rfc9110#name-422-unprocessable-content
This commit is contained in:
jordi fita mas 2024-08-27 11:07:39 +02:00
parent 790417e12c
commit 0b74c7a91c
10 changed files with 25 additions and 54 deletions

View File

@ -271,9 +271,7 @@ func handleAddPaymentAccount(w http.ResponseWriter, r *http.Request, _ httproute
return return
} }
if !form.Validate(r.Context(), conn) { if !form.Validate(r.Context(), conn) {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
form.MustRender(w, r) form.MustRender(w, r)
return return
} }
@ -313,9 +311,7 @@ func handleEditPaymentAccount(w http.ResponseWriter, r *http.Request, params htt
return return
} }
if !form.Validate(r.Context(), conn) { if !form.Validate(r.Context(), conn) {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
form.MustRender(w, r) form.MustRender(w, r)
return return
} }

View File

@ -323,9 +323,7 @@ func HandleCompanyTaxDetailsForm(w http.ResponseWriter, r *http.Request, _ httpr
return return
} }
if ok := form.Validate(r.Context(), conn); !ok { if ok := form.Validate(r.Context(), conn); !ok {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderTaxDetailsForm(w, r, form) mustRenderTaxDetailsForm(w, r, form)
return return
} }
@ -507,9 +505,7 @@ func handleCompanyInvoicingForm(w http.ResponseWriter, r *http.Request, _ httpro
return return
} }
if ok := form.Validate(); !ok { if ok := form.Validate(); !ok {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
form.MustRender(w, r) form.MustRender(w, r)
return return
} }
@ -669,9 +665,7 @@ func HandleAddCompanyTax(w http.ResponseWriter, r *http.Request, _ httprouter.Pa
return return
} }
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
page := newTaxesPageWithForm(r.Context(), conn, company, form) page := newTaxesPageWithForm(r.Context(), conn, company, form)
page.MustRender(w, r) page.MustRender(w, r)
return return
@ -802,9 +796,7 @@ func HandleAddPaymentMethod(w http.ResponseWriter, r *http.Request, _ httprouter
return return
} }
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
page := newPaymentMethodsPageWithForm(r.Context(), conn, company, form) page := newPaymentMethodsPageWithForm(r.Context(), conn, company, form)
page.MustRender(w, r) page.MustRender(w, r)
return return

View File

@ -97,9 +97,7 @@ func HandleAddContact(w http.ResponseWriter, r *http.Request, _ httprouter.Param
return return
} }
if !form.Validate(r.Context(), conn) { if !form.Validate(r.Context(), conn) {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderNewContactForm(w, r, form) mustRenderNewContactForm(w, r, form)
return return
} }

View File

@ -426,9 +426,7 @@ func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprout
return return
} }
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderEditExpenseForm(w, r, slug, form) mustRenderEditExpenseForm(w, r, slug, form)
return return
} }
@ -629,9 +627,7 @@ func handleExpenseAction(w http.ResponseWriter, r *http.Request, action string,
renderForm(w, r, form) renderForm(w, r, form)
case "add": case "add":
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
renderForm(w, r, form) renderForm(w, r, form)
return return
} }

View File

@ -617,9 +617,7 @@ func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Param
return return
} }
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderNewInvoiceForm(w, r, form) mustRenderNewInvoiceForm(w, r, form)
return return
} }
@ -1287,9 +1285,7 @@ func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprout
return return
} }
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderEditInvoiceForm(w, r, slug, form) mustRenderEditInvoiceForm(w, r, slug, form)
return return
} }

View File

@ -533,9 +533,7 @@ func handleAddPaymentForm(w http.ResponseWriter, r *http.Request, conn *Conn, co
return return
} }
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
form.MustRender(w, r) form.MustRender(w, r)
return return
} }
@ -613,9 +611,7 @@ func handleEditPaymentForm(w http.ResponseWriter, r *http.Request, conn *Conn, f
return return
} }
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
form.MustRender(w, r) form.MustRender(w, r)
return return
} }

View File

@ -95,9 +95,7 @@ func HandleAddProduct(w http.ResponseWriter, r *http.Request, _ httprouter.Param
return return
} }
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderNewProductForm(w, r, form) mustRenderNewProductForm(w, r, form)
return return
} }
@ -145,9 +143,7 @@ func HandleUpdateProduct(w http.ResponseWriter, r *http.Request, params httprout
return return
} }
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderEditProductForm(w, r, slug, form) mustRenderEditProductForm(w, r, slug, form)
return return
} }

View File

@ -121,9 +121,7 @@ func HandleProfileForm(w http.ResponseWriter, r *http.Request, _ httprouter.Para
return return
} }
if ok := form.Validate(); !ok { if ok := form.Validate(); !ok {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderProfileForm(w, r, form) mustRenderProfileForm(w, r, form)
return return
} }

View File

@ -575,9 +575,7 @@ func HandleAddQuote(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
return return
} }
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderNewQuoteForm(w, r, form) mustRenderNewQuoteForm(w, r, form)
return return
} }
@ -1064,9 +1062,7 @@ func HandleUpdateQuote(w http.ResponseWriter, r *http.Request, params httprouter
htmxRedirect(w, r, companyURI(mustGetCompany(r), "/quotes")) htmxRedirect(w, r, companyURI(mustGetCompany(r), "/quotes"))
} else { } else {
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { w.WriteHeader(http.StatusUnprocessableEntity)
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderEditQuoteForm(w, r, slug, form) mustRenderEditQuoteForm(w, r, slug, form)
return return
} }

View File

@ -701,6 +701,13 @@ htmx.on('htmx:configRequest', function (e) {
} }
}) })
htmx.on('htmx:beforeSwap', function (e) {
if (e.detail.xhr.status === 422) {
e.detail.shouldSwap = true;
e.detail.isError = false;
}
});
htmx.on('closeModal', () => { htmx.on('closeModal', () => {
const openDialog = document.querySelector('dialog[open]'); const openDialog = document.querySelector('dialog[open]');
if (!openDialog) { if (!openDialog) {