Result box: color-code by status + friendly error messages from error codes (#105)
- Backend create/can validation paths emit machine-readable error codes (slug_taken, slug_invalid, content_empty, content_too_large, expiry_invalid, rate_limited, ...) alongside the human message - new.html JS maps codes to plain-language guidance with generic fallback - Result card colored via --ok/--err/--warn left border (result-ok/err/warn) - docs/API.md error section documents the code field - Tests assert the code on every validation path
This commit is contained in:
+32
-3
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
@@ -51,6 +52,30 @@ func writeErr(w http.ResponseWriter, status int, msg string) {
|
||||
writeJSON(w, status, map[string]string{"error": msg})
|
||||
}
|
||||
|
||||
// writeErrCode emits a JSON error with a machine-readable code (#105): the
|
||||
// new-page JS maps known codes to plain-language messages, API consumers get
|
||||
// the stable `code` field alongside the human `error` text.
|
||||
func writeErrCode(w http.ResponseWriter, status int, code, msg string) {
|
||||
writeJSON(w, status, map[string]string{"error": msg, "code": code})
|
||||
}
|
||||
|
||||
// createErrCode maps a store.CreatePaste/CreateCan error to its UI error
|
||||
// code (#105).
|
||||
func createErrCode(err error) string {
|
||||
switch {
|
||||
case errors.Is(err, store.ErrSlugTaken):
|
||||
return "slug_taken"
|
||||
case errors.Is(err, store.ErrReservedSlug):
|
||||
return "slug_reserved"
|
||||
case errors.Is(err, store.ErrInvalidSlug):
|
||||
return "slug_invalid"
|
||||
case strings.Contains(err.Error(), "expires_in"):
|
||||
return "expiry_invalid"
|
||||
default:
|
||||
return "invalid_request"
|
||||
}
|
||||
}
|
||||
|
||||
// Routes returns the HTTP handler for the server.
|
||||
func (a *apiServer) Routes() http.Handler {
|
||||
return a.routes()
|
||||
@@ -158,14 +183,18 @@ func (a *apiServer) handleCreatePaste(w http.ResponseWriter, r *http.Request) {
|
||||
var p store.Paste
|
||||
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
|
||||
if isBodyTooLarge(err) { // #68: body cut off by MaxBytesReader
|
||||
writeBodyTooLarge(w)
|
||||
writeErrCode(w, http.StatusRequestEntityTooLarge, "content_too_large", "request body too large")
|
||||
return
|
||||
}
|
||||
writeErr(w, 400, "invalid json body")
|
||||
return
|
||||
}
|
||||
if status, msg := checkContent(p.Content, s.MaxContentBytes); status != 0 {
|
||||
writeErr(w, status, msg)
|
||||
if status == http.StatusRequestEntityTooLarge {
|
||||
writeErrCode(w, status, "content_too_large", msg)
|
||||
} else {
|
||||
writeErrCode(w, status, "content_empty", msg)
|
||||
}
|
||||
return
|
||||
}
|
||||
// #86: bound free-form metadata at create time
|
||||
@@ -203,7 +232,7 @@ func (a *apiServer) handleCreatePaste(w http.ResponseWriter, r *http.Request) {
|
||||
p.ViewerID = currentViewerID(r)
|
||||
created, err := a.store.CreatePaste(&p)
|
||||
if err != nil {
|
||||
writeErr(w, 400, err.Error())
|
||||
writeErrCode(w, 400, createErrCode(err), err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, 201, map[string]any{
|
||||
|
||||
Reference in New Issue
Block a user