Repository navigation
Description
Context
Ask ApplyWell is failing in production on the first message:
POST /api/v1/assistant/chat
400 Bad RequestGrafana/Loki confirmed the request reaches the backend authenticated, but conversationId is null.
Production trace:
traceId=5121fb9b824c7cfd315df13473b9544d
event=VALIDATION_FAILURE
fieldErrors={conversationId=Conversation ID is required}
Spring then throws MethodArgumentNotValidException for AssistantChatRequest.conversationId.
There is a second failure in the validation handler:
Failure in @ExceptionHandler
GlobalExceptionHandler#handleValidation(MethodArgumentNotValidException)
HttpMediaTypeNotAcceptableException:
No acceptable representation
The frontend currently sends:
Accept: text/event-streamValidation happens before the controller creates the SseEmitter, so the backend cannot render the normal JSON/ProblemDetail error for an SSE-only Accept.
Current React-JobApplyTracker/main already appears to generate and validate a UUID before sending, so production behavior does not match the current source. The deployed artifact/runtime path must also be investigated.
Related frontend repo:
https://github.com/vitorhugo-dotnet/React-JobApplyTracker
Official Spring references:
- https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/content-negotiation.html
- https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-async.html
- https://docs.spring.io/spring-framework/reference/7.1/web/webmvc/mvc-ann-rest-exceptions.html
Goal
Make Ask ApplyWell robust so that:
- no request is sent without a valid conversation UUID;
- successful responses continue to stream through SSE;
- validation/auth/request errors that happen before SSE starts return a structured HTTP error;
- stale persisted history self-heals;
- the production regression is covered by tests.
Scope
Frontend
In React-JobApplyTracker:
- Guarantee a valid
conversationIdimmediately before every call tostreamAssistantMessage. - If the ID is missing, malformed, stale, or history loading/decryption fails, generate a new
crypto.randomUUID(). - Keep UUID generation in one helper/source of truth.
- Never POST
null,undefined,"", or a malformed UUID. - Persist the replacement UUID with assistant history.
- Keep
Clear conversationgenerating a fresh UUID.
Change request negotiation to allow both the successful stream and pre-stream structured errors:
Accept: text/event-stream, application/problem+json, application/jsonBefore parsing as SSE:
- check HTTP status;
- check
Content-Type; - on non-2xx, parse ProblemDetail/JSON when available;
- throw a typed
AssistantRequestError; - never feed a JSON error body into the SSE parser.
Backend
In SpringBoot-JobApplyTracker:
- Keep
@NotNull conversationId; do not make it optional. - Keep
@NotBlank message. - Ensure
MethodArgumentNotValidExceptionis rendered using the project's structured error contract when JSON/ProblemDetail is accepted. - Ensure
GlobalExceptionHandler#handleValidationdoes not itself triggerHttpMediaTypeNotAcceptableException. - Do not convert validation failures into SSE events.
Expected invalid-request behavior:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+jsonwith validation details containing at least:
{
"fieldErrors": {
"conversationId": "Conversation ID is required"
}
}The exact envelope may follow the existing API error contract.
Investigate production frontend
Verify why production sent conversationId=null while current main contains guards.
Check at minimum:
- deployed commit SHA/version;
- Cloudflare Pages/Workers source branch;
- stale build/cache/assets;
- alternate code path calling
/assistant/chat; - older IndexedDB history shape;
- service-worker/browser cache if applicable.
Do not weaken backend validation to hide a stale frontend deployment.
Observability
- Preserve
traceId. - Keep concise validation logs.
- Do not log full prompts unnecessarily.
- Expected 4xx validation must not generate an extra stack trace from the exception handler.
Tests
Frontend
Cover:
- fresh chat creates a valid UUID before first POST;
- valid persisted UUID is reused;
- missing/malformed persisted UUID is replaced;
- decrypt/load failure creates a new UUID;
- every request body contains a valid UUID;
- Clear creates a different valid UUID;
- HTTP 400 JSON/ProblemDetail becomes
AssistantRequestError; - JSON error body is never sent to the SSE parser;
- HTTP 200
text/event-streamstill streams normally; - retry preserves the conversation ID unless the conversation is cleared.
Backend
Cover:
- missing
conversationId-> 400 structured validation response; - malformed UUID -> 400;
- blank message -> 400;
- valid request ->
text/event-stream; - validation error works with Accept including
text/event-stream,application/problem+json, andapplication/json; - validation no longer produces
HttpMediaTypeNotAcceptableException.
Acceptance criteria
- First message always sends a valid UUID.
- No frontend path can POST an empty/missing conversation ID.
- Old/invalid persisted history self-heals.
- Successful requests still use SSE.
- Pre-stream errors are structured HTTP responses.
-
GlobalExceptionHandlerno longer emitsHttpMediaTypeNotAcceptableExceptionfor this flow. - Existing retry/clear/history behavior remains functional.
- Frontend and backend tests cover the regression.
- Production deployment commit is verified.
- Grafana/Loki shows no new normal-use
conversationId requiredfailures after deploy.
Non-goals
- Changing Gemini/provider integration.
- Replacing SSE with WebSocket.
- Making
conversationIdoptional. - Redesigning the chat UI.
- Refactoring unrelated auth/error handling.
Production evidence
POST /api/v1/assistant/chat
status=400
duration=60ms
traceId=5121fb9b824c7cfd315df13473b9544d
fieldErrors={conversationId=Conversation ID is required}
HttpMediaTypeNotAcceptableException:
No acceptable representation
Activity
hugoalves-dev commentedon Sep 18, 2026
Systematic-debugging finding:
5121fb9b824c7cfd315df13473b9544doccurred at 2026-09-17 21:54:10 BRT.7657295/ PR #117 completed at about 21:50 BRT.c4fe2fe(pre-#115) still posted onlyJSON.stringify({ message })to/assistant/chat; it had noconversationIdfield. A tab/client loaded before the #115 deployment would therefore keep sending the old shape, which Spring deserializes asconversationId=null.Fixes are split into:
The post-deploy Grafana/Loki acceptance check still needs to happen after these PRs are merged/deployed.