Files
mw-pfeddersheim-workstation/docs/learnings/2026-07-06-caddy-onlyoffice-mcp-sse-investigation.md
mw 441e28bda3 docs: fix CHANGELOG duplicate Added section + missing period in learning doc
Address review feedback on ce99f58:
- Merge the two '### Added' blocks in [Unreleased] into one (Keep a
  Changelog: each category appears once). New 2026-07-06 entries now
  sit at the top of the existing Added block.
- Add missing period after 'required SSE headers' in the liveness
  verification prose (was a run-on sentence).
2026-07-06 09:14:13 +02:00

209 lines
7.7 KiB
Markdown

---
description: >-
Session record (2026-07-06): triaged Caddy journald warnings on
onlyoffice.localhost/mcp showing "context canceled" SSE aborts (~1.5ms)
from opencode 1.17.13 MCP client against the OnlyOffice MCP server on
:3847. Verified upstream liveness, opened GitLab issue #92 in
satware/mcp/onlyoffice, and documented the Caddy reverse-proxy as a
manual override.
tags:
- learning
- caddy
- onlyoffice
- mcp
- sse
- reverse-proxy
- investigation
last_updated: '2026-07-06'
---
# Caddy "context canceled" on onlyoffice.localhost/mcp - investigation - 2026-07-06
## Summary
Caddy (systemd `caddy.service`) on `mw-manjaro-pf` is emitting a high
volume of `warn`-level journald entries: `aborting with incomplete
response ... error: "reading: context canceled"` for `GET /mcp` requests
on `https://onlyoffice.localhost`. Each abort fires ~1.5ms after the
request is forwarded to the upstream `localhost:3847` (OnlyOffice MCP
server). The upstream is alive and correctly rejects non-SSE GETs with
HTTP 406, so this is a stream-lifecycle issue, not a server-down issue.
Investigation handed off to the OnlyOffice MCP repo as GitLab issue #92.
This workstation's Caddy reverse-proxy config is now recorded as a manual
override (not Ansible-managed).
## Incident
| Field | Value |
|-------|-------|
| Symptom | Repeated `context canceled` warnings in `journalctl -u caddy` |
| Caddy site | `https://onlyoffice.localhost` -> `reverse_proxy localhost:3847` |
| Upstream | `localhost:3847` (OnlyOffice MCP server) |
| Request | `GET /mcp` with `Accept: text/event-stream` |
| Client | `opencode/1.17.13` (MCP streamable-HTTP transport) |
| MCP protocol | `2025-11-25` |
| Abort duration | ~1.5ms (request forwarded, then read context canceled) |
| Frequency | Multiple per minute, across distinct `Mcp-Session-Id` values |
## Evidence
### Caddy log sample (recurring)
```json
{
"level": "warn",
"logger": "http.handlers.reverse_proxy",
"msg": "aborting with incomplete response",
"upstream": "localhost:3847",
"duration": 0.00173631,
"request": {
"method": "GET",
"host": "onlyoffice.localhost",
"uri": "/mcp",
"headers": {
"Accept": ["text/event-stream"],
"Mcp-Protocol-Version": ["2025-11-25"],
"Mcp-Session-Id": ["a5e859b4-01f1-4c24-81bb-78ee02d74357"],
"User-Agent": ["opencode/1.17.13"],
"Via": ["1.1 Caddy"]
}
},
"error": "reading: context canceled"
}
```
Observed `Mcp-Session-Id` values (each retried over several minutes):
`a5e859b4...`, `4bfbb912...`, `3c4a0454...`, `08397c3d...`,
`ed762f08...`, `b0fae341...`.
### Upstream liveness verification
```bash
$ ss -tlnp | grep :3847
LISTEN 0 4096 0.0.0.0:3847 0.0.0.0:*
$ curl -sS -o /dev/null -w "HTTP %{http_code} in %{time_total}s\n" \
--max-time 3 http://localhost:3847/mcp
HTTP 406 in 0.004665s
```
HTTP 406 is expected for a plain GET without the required SSE headers.
The MCP server is alive and rejecting non-SSE requests correctly.
## Diagnosis
### Root cause (not yet confirmed - under investigation)
Five candidate hypotheses, listed in the GitLab issue. The leading
suspect is **client-side disconnect**: opencode 1.17.13 opens the SSE
stream then immediately closes it (the ~1.5ms duration is too short for
a genuine read). Secondary suspects are server-side stream teardown,
MCP protocol-version mismatch (`2025-11-25`), Caddy HTTP/1.1 framing of
SSE, or a client session-pool bug creating and abandoning sessions.
### Why this workstation's concern
The Caddy reverse-proxy is the boundary that surfaces the symptom. Even
if the root cause is client- or server-side, the local config matters
because:
1. Caddy logs are being polluted with warn-level noise (masks real errors).
2. The reverse-proxy block has no SSE-specific tuning (`flush_interval`,
explicit HTTP/1.1 transport) that could mask or mitigate upstream
misbehavior.
3. The Caddy config at `/etc/caddy/conf.d/localhost-dev.conf` is **not**
Ansible-managed by this repo - it is a manual override that must be
recorded (see `docs/tech/manual-overrides.md`).
## Environment
| Component | Version / Value |
|-----------|----------------|
| Host | `mw-manjaro-pf` (mw-pfeddersheim-workstation) |
| OS | Manjaro Linux, Wayland session |
| Caddy | systemd `caddy.service`, active since 2026-07-02 |
| Caddy config | `/etc/caddy/Caddyfile` + `/etc/caddy/conf.d/localhost-dev.conf` |
| TLS cert | mkcert pair at `/etc/caddy/localhost{,-key}.pem` |
| Upstream | `localhost:3847` (OnlyOffice MCP server, process TBD) |
| MCP client | `opencode/1.17.13` |
| MCP protocol | `2025-11-25` (streamable HTTP transport) |
## *.localhost sites configured
All defined in `/etc/caddy/conf.d/localhost-dev.conf` (14 sites total):
| Site | Backend / Handler |
|------|-------------------|
| `mcp.localhost` | `reverse_proxy localhost:50880` |
| `artifacts.localhost` | `file_server browse` root `/var/www/artifacts` |
| `onlyoffice.localhost` | `reverse_proxy localhost:3847` (subject of this issue) |
| `project-1.localhost` ... `project-10.localhost` | `respond "project-N - placeholder"` (10 reserved) |
| `demo.satware.com.localhost` | `reverse_proxy localhost:38081` (webdevops/php-apache-dev container) |
## Handoff
### GitLab issue
| Item | Value |
|------|-------|
| Project | `satware/mcp/onlyoffice` (`gitlab.satware.com`) |
| Issue | [#92](https://gitlab.satware.com/satware/mcp/onlyoffice/-/work_items/92) |
| Title | Investigation: Caddy reports 'context canceled' on onlyoffice.localhost/mcp SSE stream (~1.5ms aborts) |
| Labels | `bug`, `investigation`, `mcp` |
| State | open |
The issue body contains the full evidence, 5 hypotheses, 7 investigation
tasks, and a proposed Caddy mitigation (`flush_interval -1` +
`transport http { versions 1.1 }`).
### Next actions (assigned to OnlyOffice MCP repo)
1. Identify the process listening on `:3847` (`sudo ss -tlnp | grep 3847`).
2. Collect server-side logs for the `Mcp-Session-Id` values cited above.
3. Reproduce with `curl -N -H "Accept: text/event-stream" -H "Mcp-Protocol-Version: 2025-11-25" https://onlyoffice.localhost/mcp`.
4. Check opencode 1.17.13 changelog for MCP SSE session bugs.
5. Verify Caddy `reverse_proxy` SSE tuning (proposed block in issue #92).
6. Confirm whether `notifications/initialized` round-trip completes.
7. Log the negotiated MCP protocol version server-side.
## Local documentation updates
| File | Change |
|------|-------|
| `docs/tech/manual-overrides.md` | Added row for `/etc/caddy/` config (not Ansible-managed) |
| `docs/tech/stack.md` | Added "Local Dev Services" section listing Caddy + `*.localhost` sites |
| `CHANGELOG.md` | Added `[Unreleased] -> Added` entry |
| GitLab issue #92 | Created with full evidence + hypotheses + tasks |
## Proposed Caddy mitigation (to test on this workstation)
```caddyfile
https://onlyoffice.localhost {
tls /etc/caddy/localhost.pem /etc/caddy/localhost-key.pem
reverse_proxy localhost:3847 {
flush_interval -1
transport http {
versions 1.1
}
}
}
```
`flush_interval -1` disables Caddy's buffering so SSE events flush to the
client immediately; `transport http { versions 1.1 }` pins the upstream
leg to HTTP/1.1 (Caddy logs already show HTTP/1.1 for the upstream, this
makes it explicit and avoids HTTP/2 SSE framing surprises).
Not applied yet - pending GitLab issue #92 resolution to avoid masking
the root cause.
## References
- GitLab issue: https://gitlab.satware.com/satware/mcp/onlyoffice/-/work_items/92
- Caddy config: `/etc/caddy/conf.d/localhost-dev.conf`
- Local skill: `caddy` (`~/.config/opencode/skills/caddy/SKILL.md`)
- MCP transport spec: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports
- Manual overrides: `docs/tech/manual-overrides.md`
- Software stack: `docs/tech/stack.md`