compat: stop mangling refusal messages with HTML escapes
server-release / image (push) Successful in 14s
server-test / test (push) Successful in 29s
server-release / release (push) Successful in 31s

The server's 426 body reached the user as "needs \u003e= 0.2.0, \u003c 1.0.0":
Go escapes <, > and & by default for JSON destined for a page, which this is
not. Disabled at the encoder. The client now parses the error field rather than
pattern-matching it, so it survives whatever a future encoder decides to escape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-01 11:41:00 +02:00
co-authored by Claude Fable 5
parent 9d6572bc33
commit 33a6acb0bf
2 changed files with 18 additions and 8 deletions
@@ -4,6 +4,8 @@
package app.echo_lot.protocol package app.echo_lot.protocol
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import java.net.URL import java.net.URL
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@@ -59,13 +61,16 @@ class ControlClient(
if (conn.responseCode == 426) throw VersionRefused(extractError(body) ?: body.take(200)) if (conn.responseCode == 426) throw VersionRefused(extractError(body) ?: body.take(200))
} }
/** Pulls the "error" string out of a JSON body without pulling in a parser for one field. */ /**
private fun extractError(body: String): String? = * Pulls the "error" string out of a JSON body.
Regex(""""error"\s*:\s*"((?:[^"\\]|\\.)*)"""").find(body) *
?.groupValues?.get(1) * Parsed rather than pattern-matched: an encoder may legitimately escape characters in the
?.replace("\\\"", "\"") * message (Go escapes ">" by default), and a regex hands the user "needs \u003e= 0.2.0".
?.replace("\\n", "\n") * The parser knows how to undo every escape; a regex would have to be taught each one.
?.replace("\\\\", "\\") */
private fun extractError(body: String): String? = runCatching {
json.parseToJsonElement(body).jsonObject["error"]?.jsonPrimitive?.content
}.getOrNull()
/** /**
* Reads the response body, and turns a 426 into [VersionRefused] first. * Reads the response body, and turns a 426 into [VersionRefused] first.
+6 -1
View File
@@ -430,7 +430,12 @@ func bearer(r *http.Request) string {
func writeJSON(w http.ResponseWriter, code int, v any) { func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code) w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v) enc := json.NewEncoder(w)
// Go escapes <, > and & by default, for JSON embedded in HTML. This is an API, and the
// escaping is actively harmful here: a refusal message reading "needs >= 0.2.0" is what
// the user ends up seeing. Nothing we emit is ever interpolated into a page.
enc.SetEscapeHTML(false)
_ = enc.Encode(v)
} }
// enroll redeems a single-use enrollment token for a device credential (§2.1). // enroll redeems a single-use enrollment token for a device credential (§2.1).