Context
Ask ApplyWell is failing in production on the first message:
POST /api/v1/assistant/chat
400 Bad Request
Grafana/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-stream
Validation 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:
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
conversationId immediately before every call to streamAssistantMessage.
- 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 conversation generating 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/json
Before 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
MethodArgumentNotValidException is rendered using the project's structured error contract when JSON/ProblemDetail is accepted.
- Ensure
GlobalExceptionHandler#handleValidation does not itself trigger HttpMediaTypeNotAcceptableException.
- Do not convert validation failures into SSE events.
Expected invalid-request behavior:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
with 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-stream still 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, and application/json;
- validation no longer produces
HttpMediaTypeNotAcceptableException.
Acceptance criteria
Non-goals
- Changing Gemini/provider integration.
- Replacing SSE with WebSocket.
- Making
conversationId optional.
- 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
Context
Ask ApplyWell is failing in production on the first message:
Grafana/Loki confirmed the request reaches the backend authenticated, but
conversationIdisnull.Production trace:
Spring then throws
MethodArgumentNotValidExceptionforAssistantChatRequest.conversationId.There is a second failure in the validation handler:
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-onlyAccept.Current
React-JobApplyTracker/mainalready 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:
Goal
Make Ask ApplyWell robust so that:
Scope
Frontend
In
React-JobApplyTracker:conversationIdimmediately before every call tostreamAssistantMessage.crypto.randomUUID().null,undefined,"", or a malformed UUID.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:
Content-Type;AssistantRequestError;Backend
In
SpringBoot-JobApplyTracker:@NotNull conversationId; do not make it optional.@NotBlank message.MethodArgumentNotValidExceptionis rendered using the project's structured error contract when JSON/ProblemDetail is accepted.GlobalExceptionHandler#handleValidationdoes not itself triggerHttpMediaTypeNotAcceptableException.Expected invalid-request behavior:
with 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=nullwhile currentmaincontains guards.Check at minimum:
/assistant/chat;Do not weaken backend validation to hide a stale frontend deployment.
Observability
traceId.Tests
Frontend
Cover:
AssistantRequestError;text/event-streamstill streams normally;Backend
Cover:
conversationId-> 400 structured validation response;text/event-stream;text/event-stream,application/problem+json, andapplication/json;HttpMediaTypeNotAcceptableException.Acceptance criteria
GlobalExceptionHandlerno longer emitsHttpMediaTypeNotAcceptableExceptionfor this flow.conversationId requiredfailures after deploy.Non-goals
conversationIdoptional.Production evidence