Skip to content

fix(assistant): prevent null conversationId and handle pre-SSE errors correctly #96

Description

@vitorhugo-dotnet

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:

  1. no request is sent without a valid conversation UUID;
  2. successful responses continue to stream through SSE;
  3. validation/auth/request errors that happen before SSE starts return a structured HTTP error;
  4. stale persisted history self-heals;
  5. 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

  • 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.
  • GlobalExceptionHandler no longer emits HttpMediaTypeNotAcceptableException for 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 required failures after deploy.

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

Activity

  1. vitorhugo-dotnet commented on Sep 18, 2026

    @vitorhugo-dotnet
    OwnerAuthor

    Systematic-debugging finding:

    • Production Loki trace 5121fb9b824c7cfd315df13473b9544d occurred at 2026-09-17 21:54:10 BRT.
    • Cloudflare Worker deployment containing frontend commit 7657295 / PR #117 completed at about 21:50 BRT.
    • So the Worker was already current when the failure happened.
    • Looking at the frontend history, commit c4fe2fe (pre-#115) still posted only JSON.stringify({ message }) to /assistant/chat; it had no conversationId field. A tab/client loaded before the #115 deployment would therefore keep sending the old shape, which Spring deserializes as conversationId=null.
    • This matches the production validation log exactly and is stronger evidence than a wrong Cloudflare source branch.

    Fixes are split into:

    The post-deploy Grafana/Loki acceptance check still needs to happen after these PRs are merged/deployed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions