element-hq / element-hq/element-web

QR login: existing device gets 412 "ETag does not match" on rendezvous PUT and does not retry

Open
#34,453 2 comments 0 reactions 0 assignees View on GitHub
A-Login O-Uncommon S-Major T-Defect
Dominant language
TypeScript
Stars
13.5k
Forks
2.8k
PR merge metrics
PR metrics pending

Description

### Steps to reproduce

1. Self-hosted Synapse with MAS (OAuth delegation), `experimental_features: msc4108_enabled: true`
2. Element Web → Settings → Sessions → "Link new device"
3. Scan the QR code with Element X Android

### Outcome

**Expected:** the device is linked.

**What happened:** Element X stays on "Establishing a secure connection" until the
login attempt expires ("The linking wasn't completed in the required time").
Reproducible over both Wi-Fi and mobile data.

### Server-side log

The rendezvous channel opens, the new device writes successfully, then the
existing device fails to write and never recovers:

```
17:47:12,545 ElementX GET 200 0B /_synapse/client/rendezvous/01KYMPH0...
17:47:12,682 ElementX PUT 202 /_synapse/client/rendezvous/01KYMPH0...
17:47:12,765 ElementX GET 304 0B /_synapse/client/rendezvous/01KYMPH0...
17:47:13,509 Firefox GET 200 104B /_synapse/client/rendezvous/01KYMPH0...
17:47:13,547 Firefox PUT 412 103B /_synapse/client/rendezvous/01KYMPH0...
SynapseError: 412 - ETag does not match
17:47:13,583 Firefox GET 304 0B /_synapse/client/rendezvous/01KYMPH0...
[both sides poll with 304 for ~53s, no further PUT from either side]
17:48:06,015 ElementX GET 404 (session expired)
```

Two things stand out:

1. Element Web reads the payload (200, 104B) and 38 ms later sends a PUT whose
`If-Match` does not match — yet its very next GET returns 304, so it *does*
hold the current ETag.
2. After the 412 it never retries. In this protocol 412 is the normal
"someone else wrote first, re-read and try again" response, so giving up
here deadlocks the handshake.

The same pattern repeats in every attempt: the device that reads an empty
session writes fine; the device that reads a non-empty payload gets 412.

### Server side ruled out

`org.matrix.msc4108: true` in `/_matrix/client/versions`. Manually replaying the
exact operation against the same endpoint through the same reverse proxy
succeeds, with a payload larger than the 104 bytes Element Web chokes on:

```console
$ URL=$(curl -s -D h -X POST -H 'Content-Type: text/plain' --data 'x' \
https://$SERVER/_matrix/client/unstable/org.matrix.msc4108/rendezvous | jq -r .url)
$ ET=$(grep -i '^etag:' h | tr -d '\r' | sed 's/^[^:]*: //')
$ curl -s -o /dev/null -w 'PUT %{http_code}\n' -X PUT -H 'Content-Type: text/plain' \
-H "If-Match: $ET" --data "$(printf 'a%.0s' {1..200})" "$URL"
PUT 202
$ curl -si -H 'Accept-Encoding: gzip, deflate, br' "$URL" | grep -iE '^(etag|content-encoding)'
etag: "wqkI2Y9d-Yet5BtfziEwZ--8wh7yJAISpB5UtefCiuU"
```

Strong ETag, no `Content-Encoding` despite the client offering gzip/br — the
proxy rewrites neither the ETag nor the body. Compression-weakened ETags
(`W/"..."`, which would legitimately fail `If-Match` under RFC 9110) are
therefore not the cause. Reverse proxy has `gzip off`, `proxy_buffering off`
and `proxy_request_buffering off` on both rendezvous paths.

Possibly related: matrix-org/matrix-js-sdk#5186 (supporting both the 2024 and
2025 revisions of MSC4108) — if Element Web and the Rust SDK in Element X
negotiate different revisions of the secure channel, that would explain the
ETag mismatch.

### Versions

- Element Web: 1.12.24 (Firefox 153, Linux/Windows)
- Element X Android: 26.07.1 (Google Pixel Fold, Android 17)
- Synapse: 1.157.2
- MAS: 1.21.0
- Deployment: self-hosted, nginx reverse proxy

### Operating system

_No response_

### Browser information

_No response_

### URL for webapp

1.12.24

### Application version

Element X Android: 26.07.1

### Homeserver

1.157.2

### Will you send logs?

Yes

Contributor guide

Open the contributing guide

Research direction

Start by tracing Element Web's rendezvous PUT and ETag handling, then compare its MSC4108 revision negotiation with matrix-org/matrix-js-sdk#5186. Reproduce the 412 after a non-empty payload and verify that the secure-link handshake recovers by re-reading and retrying; done means the QR login completes instead of expiring.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
authentication, frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.