Back to Feed
article 4m read

Chore list shipped where one bad page load could wipe it

Moving my scheduling app's chores onto the server, I let a list-shaped request replace the whole file with no checks, no lock and no read-back, and the browser starts from an empty list when loading fails. What I found rereading the commits, and the shape I want instead.

Article DepthPractice

Flow

What my chore save is missing

  1. 1One-item commands

    Add and delete one chore at a time, with the server picking ids.

  2. 2No blind replace

    Drop the replace-everything path, or make it name the version it started from.

  3. 3A lock

    Hold one lock around the read and the write.

  4. 4Temp file, then rename

    Never leave a half-written file where a reader can find it.

  5. 5Honest loading

    Block saving after a failed load instead of starting from an empty list.

  6. 6Read the answer

    Keep an edit unsaved until the server says yes.

In one day, the chore list in my staff-scheduling app went from a few chores written into the JavaScript to a file on the server. Reading those commits back, one branch bothers me most. If the browser sends a list, the server throws out what it has and saves that list instead. It checks nothing first.

From one item to everything

The first version only handled one chore at a time. It checked the token, took a chore with an id, updated it or added it, and saved the file. Delete removed one chore by id.

Then the browser started sending the whole chore list on every add and every delete. That came with the change that moved chore loading onto the API. Later, when I added assignees and due dates, the server learned a second meaning for the same request. Send one object and it updates one chore. Send a list and it replaces everything.

If the body is a list, it replaces everything.

That one line is the most dangerous thing the endpoint can do, and it has the fewest guards.

Five ways the list can get eaten

Nothing checks the items in the list. The token only says who is allowed to call.

Nothing stops two saves from running at once. The readme lists a queue file used for locking when saving, but the chore code doesn't touch it. Two saves that overlap end with whoever lands last. The browser also makes new ids by adding one to the biggest id it has, so two open tabs could pick the same one.

The browser's error handling is the one that worries me. If loading the chores fails, the page just starts with an empty list and says nothing. The next add sends a one-item list, and the server makes that the whole list. As far as I can read the code, one bad page load and one click wipe the file. I haven't seen that happen. I haven't looked, either. There's a rarer version I can only guess at. The server writes over the file in place, and if a read hits a half-written file, it gets treated as empty too.

The save never checks the answer. A rejected save looks just like a good one.

And I suspect the list check needs a newer PHP than I assumed. On an older server, that branch would break before it checked anything.

Two startup rewrites in one day

Two commits that day both changed how the page starts. One loads staff, schedule and chores together, waits for all of them, picks today's date, then draws. The other loads them one after another. Before that, the page could pick a date before the chores had loaded. I like the version that waits for everything. But neither one fixes the empty-list problem, because waiting doesn't tell "no chores" apart from "couldn't load chores."

The shape I want instead

If you're building something like this, these are the parts I'd want in place:

  • Separate commands for adding and deleting one item, with the server picking the ids.
  • No full replace, or one that must say which version it started from.
  • A lock around the read and the write.
  • Write to a temp file, then rename it over the real one.
  • A browser that blocks saving after a failed load and keeps edits until the server says yes.

I'll probably just delete the replace branch, since every caller I can find only adds or removes one chore.

Before I touch anything, I want to know which PHP that server actually runs.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward