Back to the blog

Fixing a Missing User Pool ID Behind an Empty Sites Dropdown

An empty sites dropdown traced back to a missing user pool ID: the users request failed, and the screen’s Promise.all rejection path left a successful sites response unused.

Introduction

I traced a misleading warning on an admin users screen to a backend configuration problem and the way the frontend handled two requests together. The screen said “no sites are available to assign,” even though the sites request had succeeded. It fetched users and sites through Promise.all, and the users request failed because the backend had no configured Cognito user pool ID. The aggregate promise rejected, so the screen’s result-handling path never applied the successful sites response to UI state.

I fixed the immediate configuration problem by adding the correct COGNITO_CAMPER_USER_POOL_ID to my local .env file and restarting the API. After that, the page functioned properly and sites populated as expected. This supplied the missing configuration; it did not establish that the screen’s loading behavior or the service’s fallback had been rewritten. The local recovery showed that the configuration fix worked, not that the screen could now handle independent request failures independently.

The Rule That Shipped

When COGNITO_CAMPER_USER_POOL_ID is unset, CamperUsersService substitutes an empty string. The affected backend user lookup passes that value to Cognito, which rejects the empty pool ID with an invalid parameter error. The fix was to supply the correct pool ID rather than rely on the fallback. I added it to the local .env file and also set it in the lambda environment file.

This lookup requires the correct user pool ID in the environment. The empty string satisfies the service’s substitution logic, but it is not valid for the demonstrated Cognito query. Supplying the correct value resolved the problem; that does not mean any nonempty value would work.

I have not established that the empty-string fallback was removed or that startup validation was added. The local recovery also does not establish a deployed lambda outcome. The confirmed change was configuration in both files, with recovery directly observed after restarting the local API.

Worked Example

With COGNITO_CAMPER_USER_POOL_ID unset, the admin screen starts its users and GET /admin/sites requests and combines their results with Promise.all. On the backend, the users handler reaches CamperUsersService. The service substitutes an empty string for the missing pool ID and passes it to Cognito. Cognito rejects the lookup with an invalid parameter error, causing the users request to fail. This is a backend dependency failure, not a browser request sent directly to Cognito.

Meanwhile, GET /admin/sites succeeds and returns site data. When the users request rejects, Promise.all rejects the aggregate promise instead of fulfilling with an array of both results. It does not cancel the other request or erase its response. But in this screen’s aggregate-rejection path, the successful sites response is discarded rather than applied to UI state. Sites remain unpopulated, and the screen shows “no sites are available to assign” instead of identifying the missing configuration.

The changed input was the correct COGNITO_CAMPER_USER_POOL_ID in the local environment, followed by an API restart. With that configuration in place, the page functioned properly and sites populated. That is the observed before-and-after result. It does not require an assumed response payload or a claim about internal state updates that were not separately verified.

The exact Cognito error text and response shape remain unknown. What is established is an invalid-parameter failure caused by the empty pool ID, not a particular message, status code, or stack trace.

Challenges and Tradeoffs

Promise.all gives concurrent reads a simple success path: when every input fulfills, the caller receives the results together. Rejecting the aggregate is useful when the caller needs every result. Here, though, users and sites were independent reads, and the screen’s handling made applying site data depend on both requests succeeding. Concurrency was not the problem. Treating the aggregate as the only usable outcome was.

One alternative would be Promise.allSettled, with explicit handling for each fulfilled or rejected result. Another would be separate success and error paths for the two requests. Either could preserve usable site data while reporting the users failure, but neither is established as shipped. Both also require a UI decision: available sites do not necessarily mean user creation should proceed while another required operation is unavailable.

Fail-fast configuration validation is a separate option. Rejecting a missing pool ID before the affected lookup would make the dependency clearer. Startup-wide validation, however, could prevent unrelated functionality from starting. Choosing between startup validation and validation at the lookup boundary means deciding how broadly the failure should affect the application; neither approach is established as implemented. I kept the account of the actual fix narrower: the correct configuration restored the observed local behavior. The fallback and aggregate-result handling remain limitations, not completed repairs.

Verification Process

My direct verification was a local before-and-after check. I added the correct COGNITO_CAMPER_USER_POOL_ID to .env, restarted the API, and confirmed that the admin users page functioned properly and populated sites. The restart is part of that sequence: the observed recovery followed both the configuration edit and the restart, not the file edit alone.

That check confirms recovery from this missing-configuration case. It does not establish regression coverage for partial request failures. A targeted follow-up would make the users request fail while allowing GET /admin/sites to succeed, then check whether the screen applies the site data and distinguishes a loading failure from an empty list. A separate missing-variable check would establish whether clearer validation has since been added. These are gaps in verification, not tests I have completed.

I also observed that GET /admin/sites lacked a siteType parameter. Whether that was corrected remains unclear, so it is not part of the confirmed configuration repair. Later work on a new patrol-camera endpoint, welcome email changes, and an SES sender fix does not establish coverage of this failure either. Whether those changes were specifically verified against the same Promise.all coupling bug remains unknown.

Conclusion

I fixed the demonstrated local failure by supplying the correct user pool ID and restarting the API. The page then functioned properly, with sites populated. The misleading warning came from a backend Cognito configuration error combined with a frontend rejection path that left successful site data unused—not from Promise.all destroying responses.

The limits are still explicit: removal of the fallback is not established, independent-result handling is not established as repaired, and broader deployment verification is not demonstrated. The exact Cognito error text and response shape, the disposition of the missing siteType parameter, and regression coverage from later related changes remain unresolved.