DEV Community

Cover image for Fixing a Hidden State Mutation Bug in StayPresent's Web API
John Wick
John Wick

Posted on

Fixing a Hidden State Mutation Bug in StayPresent's Web API

Summer Bug Smash: Clear the Lineup ๐Ÿ›๐Ÿ›น

This is a submission for *DEV's Summer Bug Smash: Clear the Lineup** powered by Sentry.*

Preventing Accidental Mutation of Internal State in StayPresent

Project Overview

StayPresent is an open-source Python library for running and monitoring long-running bots while exposing their status through a lightweight built-in web interface.

The library is designed around predictable behavior and reliability. Since it often runs for days or weeks at a time, seemingly small implementation details can have surprisingly large consequences.

While auditing the codebase, I discovered a subtle bug where user code could unintentionally modify the framework's own internal state.


The Bug

StayPresent provides staypresent.web.get() and staypresent.web.get_all() so applications can inspect the currently served state.

At first glance, these functions appeared to return an independent snapshot of that state.

In reality, they only made a shallow copy of the outer response object.

For JSON responses, the nested "value" object remained the exact same dictionary or list stored internally by StayPresent.

That meant code as ordinary as this:

state = staypresent.web.get()
state["value"]["count"] += 1
Enter fullscreen mode Exit fullscreen mode

didn't just modify the local variable.

It silently modified the live data that StayPresent would serve to every future HTTP request.

No additional API call was required.

No warning was produced.

The framework's internal state had effectively escaped into user code.

The most concerning part was that the mutation happened completely outside the framework itself, making the resulting behavior difficult to trace back to the real cause.


The Fix

The solution was intentionally simple.

Instead of returning a shallow copy, get() and get_all() now return a deep copy of the stored state.

Every caller receives an independent snapshot that can be inspected or modified freely without affecting StayPresent's internal data.

This restores the behavior users naturally expect: reading state should never mutate it.


Why It Matters

This wasn't a crash or an exception.

The application continued running normally, but its behavior could quietly change because of an innocent-looking modification to a returned object.

Bugs like these are especially dangerous because they violate one of the most fundamental expectations of an API: that a getter doesn't expose ownership of the underlying data.

Fixing the issue not only prevents subtle data corruption, but also makes the API significantly easier to reason about.


What I Learned

This bug reinforced an important lesson: defensive copying isn't just an implementation detailโ€”it's part of an API's contract.

When an API appears to return a snapshot, developers naturally assume they own the returned object.

If internal mutable state leaks across that boundary, seemingly harmless application code can produce behavior that's extremely difficult to diagnose.

By returning independent copies instead of shared references, StayPresent now behaves consistently with those expectations while preserving its existing public API.


Code

GitHub Repository

https://github.com/StayElite/StayPresent

Changelog (Fixed in v1.5.16)

https://github.com/StayElite/StayPresent/blob/main/CHANGELOG.md

Commit

https://github.com/StayElite/StayPresent/commit/def77e96a722de3a6705ed6dd4b654e82142adc6

Top comments (0)