Asynchronous File Processing with WebSocket Feedback (Ticket Pattern)

This example shows why WebSocket is useful for long-running file processing, without uploading files over WebSocket.

Problem

  • Uploading files over WebSocket is not the intended workflow.

  • File upload happens over HTTP (REST).

  • Processing can be asynchronous and may finish quickly.

If the client uploads first and subscribes later, it can miss events when processing finishes quickly.

Solution: ticket-first workflow

Before uploading, the client asks over WebSocket for a ticket that binds:

  • the upcoming HTTP upload

  • the processing job

  • the WebSocket subscription

The client then registers before uploading, using the ticket as a filter.

Important: you only receive server-emitted status updates if you have registered the corresponding event resources (via register). If you want updates for multiple stage commands, register them all (or design a single aggregated command that you register once).

Important: when the server responds to a client command, it replies with the original event fields, camelCase-transformed, and adds a nested response object. There is no top-level status; status is inside response.status.

Flow

  1. WS: request a document-ticket (creates a processing correlation ID)

  2. WS: register for status events filtered by that ticket (required)

  3. HTTP: upload the file and include the ticket

  4. WS: receive stage-specific processing events (document-preparing, document-ocr, document-ready)


1) WS: request a ticket

Client:

{
  "command": "document-ticket"
}

Server:

{
  "command": "document-ticket",
  "response": {
    "data": {
      "ticket": "c3b7b2c0-7b9b-4c5d-8b52-1f94d98b9e2a"
    },
    "status": "OK"
  }
}

2) WS: register before upload (required)

Client:

{
  "command": "register",
  "event": "document-ready",
  "param_set": {
    "ticket": "c3b7b2c0-7b9b-4c5d-8b52-1f94d98b9e2a"
  }
}

Server replies with the original event fields (camelCase-transformed) and a nested response object:

{
  "command": "register",
  "event": "document-ready",
  "paramSet": {
    "ticket": "c3b7b2c0-7b9b-4c5d-8b52-1f94d98b9e2a"
  },
  "response": {
    "status": "OK"
  }
}

3) HTTP: upload with ticket

Upload the file over REST and include the ticket so the server can bind the processing job to your WS subscription.

Pick one convention in your application and document it:

  • header

  • query parameter

  • form field


4) WS: receive status events

Server pushes use the same envelope:

After the upload is accepted, the server emits stage-specific commands as processing progresses (for example document-preparing, document-ocr, and finally document-ready).

During file handling, the server may emit different commands for different stages.

Example: preparing (server accepted upload and queued work)

{
  "command": "document-preparing",
  "ticket": "c3b7b2c0-7b9b-4c5d-8b52-1f94d98b9e2a",
  "response": {
    "status": "OK"
  }
}

Example: OCR started

{
  "command": "document-ocr",
  "ticket": "c3b7b2c0-7b9b-4c5d-8b52-1f94d98b9e2a",
  "response": {
    "data": {
      "progress": null
    },
    "status": "OK"
  }
}

Example: OCR progress

document-ocr may be emitted multiple times during processing. For example, it can be sent on every progress update (e.g. 0–100%), potentially dozens or hundreds of times.

{
  "command": "document-ocr",
  "ticket": "c3b7b2c0-7b9b-4c5d-8b52-1f94d98b9e2a",
  "response": {
    "data": {
      "progress": 65
    },
    "status": "OK"
  }
}

Example: extraction finished

{
  "command": "document-ready",
  "ticket": "c3b7b2c0-7b9b-4c5d-8b52-1f94d98b9e2a",
  "response": {
    "data": {
      "documentId": 123
    },
    "status": "OK"
  }
}

This pattern allows the client to:

  • show real-time progress per stage

  • react immediately when processing completes

  • avoid polling entirely