fix(profile): validate TS6 HTTP status and stop escaping JSON body

Silent failure: logs showed "Client properties updated" / "Description
updated" / "Avatar updated" even though the bot's nickname never
changed and the avatar stayed as "loading image" on clients.

Root causes:
- TS6HttpQuery.clientUpdate ignored non-2xx responses, so 400 (bad
  parameter) and 403 (insufficient permission) were reported as success.
- updateClientProperties built TS3-escaped strings (\\s for space) then
  split them back into JSON props, so TS6 received literal backslashes
  and rejected the nickname silently.
- handleFeatureError only matched textual "permission" errors; HTTP
  4xx statuses weren't recognised and the feature retried every song.
- doAvatarUpload had no per-step logging, making it impossible to tell
  whether a broken avatar came from init, the TCP 30033 transfer, or
  the client_flag_avatar command.

Fixes:
- Add HttpQueryError (status/body/path); clientUpdate throws on non-2xx.
- Build a raw property map in updateClientProperties; escape only on
  the TS3 wire path.
- Log HTTP status and updated prop names on success.
- handleFeatureError now treats HTTP 400/401/403 as unrecoverable.
- Debug-log each step of doAvatarUpload plus bytes/elapsedMs on success.

https://claude.ai/code/session_018NrpGWbQQTrahUVXyea5Jy
This commit is contained in:
Claude committed 2026-04-17 16:21:52 +00:00
1 parent 66ae948371
commit 314d6ec955
2 files changed
+112 -25

No files matched your search

+46 -2
View File
@@ -14,6 +14,38 @@ export interface HttpQueryResult {
body: unknown;
}
/**
* Thrown when the TS6 HTTP Query returns a non-2xx status.
*
* The previous implementation silently ignored the status code, so a 400
* (bad parameter) or 403 (insufficient permission) looked identical to
* success in logs. Callers that rely on the response being applied —
* nickname / description / away-status updates — should catch this and
* surface it rather than log "updated" for a request that was rejected.
*/
export class HttpQueryError extends Error {
readonly status: number;
readonly body: unknown;
readonly path: string;
constructor(path: string, status: number, body: unknown) {
const bodySnippet = (() => {
if (body == null) return "";
const s = typeof body === "string" ? body : JSON.stringify(body);
return s.length > 200 ? s.slice(0, 200) + "\u2026" : s;
})();
super(
`TS6 HTTP Query ${path} failed: status=${status}${
bodySnippet ? ` body=${bodySnippet}` : ""
}`,
);
this.name = "HttpQueryError";
this.status = status;
this.body = body;
this.path = path;
}
}
/**
* TS6 HTTP Query client.
*
@@ -157,12 +189,24 @@ export class TS6HttpQuery {
});
}
/** Update client properties (e.g., description) */
/**
* Update client properties (e.g., description, nickname, away).
*
* Throws HttpQueryError on non-2xx responses. The TS6 server returns
* 400 for invalid parameters and 403 for insufficient permissions;
* prior to this check the errors were silently dropped and callers
* logged a false "updated" success.
*/
async clientUpdate(
properties: Record<string, string | number>,
sid = 1,
): Promise<HttpQueryResult> {
return this.request("POST", `/1/clientupdate?sid=${sid}`, properties);
const path = `/1/clientupdate?sid=${sid}`;
const result = await this.request("POST", path, properties);
if (result.status < 200 || result.status >= 300) {
throw new HttpQueryError(path, result.status, result.body);
}
return result;
}
/** Move a client to a channel */