From c0a83b5781619903a124a45dfa1073866727d049 Mon Sep 17 00:00:00 2001 From: Tomas Balsys Date: Thu, 27 Nov 2025 23:27:29 +0200 Subject: [PATCH] Protect changing in parallel using HTTP 412 precondition failed --- examples/base/main.go | 36 +++++++++++++++++++ .../records/RecordUpsertPanel.svelte | 12 +++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/examples/base/main.go b/examples/base/main.go index 1ea529b4..c29fd4c5 100644 --- a/examples/base/main.go +++ b/examples/base/main.go @@ -116,6 +116,12 @@ func main() { Priority: 999, // execute as latest as possible to allow users to provide their own route }) + app.OnRecordViewRequest().BindFunc(addLastModified) + + app.OnRecordUpdateRequest().BindFunc(ifUnmodifiedSince) + + app.OnRecordDeleteRequest().BindFunc(ifUnmodifiedSince) + if err := app.Start(); err != nil { log.Fatal(err) } @@ -129,3 +135,33 @@ func defaultPublicDir() string { return filepath.Join(os.Args[0], "../pb_public") } + +func addLastModified(e *core.RecordRequestEvent) error { + updated := e.Record.GetString("updated") + + if updated != "" { + e.Response.Header().Add("Last-Modified", updated) + } + + return e.Next() +} + +func ifUnmodifiedSince(e *core.RecordRequestEvent) error { + updated := e.Record.GetString("updated") + + if updated != "" { + header := e.Request.Header.Get("If-Unmodified-Since") + + if header == "" || header != updated { + e.Response.Header().Add("Last-Modified", updated) + + if header == "" { + return e.Error(http.StatusPreconditionRequired, "Header If-Unmodified-Since is required", nil) + } else if header != updated { + return e.Error(http.StatusPreconditionFailed, "Record was modified after retrieval", nil) + } + } + } + + return e.Next() +} diff --git a/ui/src/components/records/RecordUpsertPanel.svelte b/ui/src/components/records/RecordUpsertPanel.svelte index 7bc9a992..15985d63 100644 --- a/ui/src/components/records/RecordUpsertPanel.svelte +++ b/ui/src/components/records/RecordUpsertPanel.svelte @@ -278,7 +278,11 @@ if (isNew) { result = await ApiClient.collection(collection.id).create(data); } else { - result = await ApiClient.collection(collection.id).update(record.id, data); + result = await ApiClient.collection(collection.id).update(record.id, data, { + headers: { + "If-Unmodified-Since": record.updated, + }, + }); } addSuccessToast(isNew ? "Successfully created record." : "Successfully updated record."); @@ -318,7 +322,11 @@ confirm(`Do you really want to delete the selected record?`, () => { return ApiClient.collection(original.collectionId) - .delete(original.id) + .delete(original.id, { + headers: { + "If-Unmodified-Since": record.updated, + }, + }) .then(() => { forceHide(); addSuccessToast("Successfully deleted record.");