Skip to content

Testing the Interface Against API Status Codes

Adityo Guni Waluyo

A duplicate email returned a perfect 409 and an English toast in an Indonesian UI. The API was right; the translation layer failed twice.

TL;DR

QA tested 22 admin scenarios; 20 passed. Two failures were frontend translation issues, not backend bugs: a 409 conflict surfaced as an untranslated English toast, and a rejected fake image upload triggered no feedback at all despite the server correctly returning 400. Silent failures mislead more than wrong-language ones; localization belongs in the interface layer.

While testing the add-user form, I entered an email address that was already registered. The screen flashed briefly, then a toast appeared in the middle of an Indonesian interface: "email already in use". The message was in English, raw from the API, with no translation whatsoever.

My first guess: the API was returning a non-standard or broken error code. After checking the network tab in devtools, the facts ran the other way. The API was speaking perfectly correct RFC status codes: 201 for a successful create, 400 for a malformed request, 403 for forbidden access, and 409 for a data conflict. StatusConflict is defined as 409 in Go's net/http package, following RFC 9110 [5] [6]. What wasn't correct was the translation layer: the frontend never mapped 409 to a proper Indonesian message, and that failure landed on the user as the wrong language.

The framing inverts if you read it too fast. The backend hadn't failed at all. An API should indeed be language-blind, speaking only protocol statuses; localization is the interface's responsibility. The two findings from this session illustrate exactly the two faces of that same translation failure.

22 Scenarios, Two Findings With Nothing to Do With Logic

This session covered 22 scenarios across six admin areas: users, media, reviews, menus, settings, and profile. Twenty passed without findings. The remaining two were both translation failures on the interface side, not on the business side.

Finding R5-1 is that English toast; the 409 response was right, the displayed message was in the wrong language. Finding R5-2 is one level more serious: a fake image file sent to the media endpoint was indeed rejected by the server with 400, but the upload panel showed nothing at all. No toast, no change; confirmed twice with identical results. The server honestly said "this request is bad", and the frontend chose total silence. Between the two, which one misleads more? A wrongly-worded toast at least says something is wrong; a silent failure makes people wait for a process that will never finish.

Some media details matter here: the fake upload, plain text renamed to a .png extension, was stopped by the server's type validation rather than slipping silently into storage. What was missing was only the feedback. A real image upload, by contrast, worked end to end: the database row was created, the physical file landed in the container, the thumbnail rendered. Title and alt metadata persisted, and deletion removed both the row and the physical file. That contrast matters for finding R4-5 in another module: the media endpoint cleans up after itself correctly; it is deletion through the parent entity that leaves the orphan behind.

The most convincing parts were the ones that produced no findings at all. The bidang guard was tested by forcing an "ALL" bidang value from an editor account: the server answered 403 and the database stayed intact. User deletion used a native confirmation with a clear destructive warning. A menu item was added and deleted until the public navigation was exactly as before, and the settings tagline was changed and restored after the old value was verified in both the database and the public API. Signing out threw the token out of localStorage, and opening an admin page without a session showed the login form at the same URL instead of leaking admin content. One session, the whole lifecycle cleaned up, no test data left behind.

Verification ran with the same pattern as the other modules: every claim carries an artifact. The password change was verified with bcrypt.checkpw [2], the cycle was tested in both directions, the new password accepted with 200 and the old one answered 401, then reverted so the reverse held true. Weak passwords were blocked client-side before a single request left the browser, which saves the server but demands that frontend and backend validation rules stay aligned. Even a sixteen-bit piece of typography didn't escape checking: the em-dash in a review note was proven stored as bytes E28094 in the database, not merely looking right on screen [4].

One QA plan expectation collapsed during testing. The old plan assumed reviews would enter a moderation queue before going public, while the actual behavior is instant publish with reactive moderation: admins pull problematic reviews with a mandatory reason, restore them, and the public sees the change immediately. That behavior is the design, and the session recorded it as a clarification, not a finding. Plans go stale; the code sets the contract.

HTTP standards are the machine's universal language. The developer's job is making sure the machine translates it into whatever language its humans actually read, including when it refuses them.

Sources

[2] pyca/bcrypt README
[4] Go spec
[5] Go net/http package
[6] golang/go status.go

Related articles