mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
`api()` 是 `(p, o) => fetch(p, o)`,第二个参数是原生 fetch options,
所以 `body` 必须是字符串。两处都传了对象字面量:
- 生图页 `body: { model, prompt, n, size }`
- 源编辑框自动探测 `body: { base_url, api_key }`
fetch 把对象转成 `"[object Object]"`,网关 json.Unmarshal 失败返回
400,请求根本没到上游。生图页的现象是 lat_ms: 0。
两处都改成 JSON.stringify + 显式 Content-Type。
判据在源码层扫描全文件的每个 `method: "POST"`,而不是断言 DOM ——
wire format 在 DOM 里不可见,这也正是为什么之前测过 curl 端点却漏掉
了按钮。这条全文件扫描当场抓到了第二个同源缺陷(自动探测按钮)。
109 lines
3.8 KiB
Go
109 lines
3.8 KiB
Go
package gateway
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestUI_ImageRequestSendsJSONBody pins the bug that made the image page
|
|
// unusable in production while every DOM assertion still passed.
|
|
//
|
|
// The page called api(path, {method, body: {...}}). api() is
|
|
// `(p, o) => fetch(p, o)` — o is the NATIVE fetch options object, so an object
|
|
// in `body` is coerced by fetch into the literal text "[object Object]". The
|
|
// gateway's json.Unmarshal rejected it, the response was a bare 400 with
|
|
// lat_ms 0, and the page showed an error toast with no image and no usable
|
|
// message.
|
|
//
|
|
// What made this expensive: the pane rendered, the model list had all six
|
|
// models, the button enabled itself, the console was clean. Only the WIRE
|
|
// format was wrong, and nothing in the DOM shows a wire format.
|
|
func TestUI_ImageRequestSendsJSONBody(t *testing.T) {
|
|
body := sendImageSource(t)
|
|
|
|
if !strings.Contains(body, "JSON.stringify") {
|
|
t.Error("sendImage() does not JSON.stringify its body — fetch coerces " +
|
|
"the object to the text \"[object Object]\" and the gateway answers " +
|
|
"400 with lat_ms 0")
|
|
}
|
|
if !strings.Contains(body, `"Content-Type": "application/json"`) {
|
|
t.Error("sendImage() does not set Content-Type: application/json; the " +
|
|
"server sees text/plain and may refuse to parse the body")
|
|
}
|
|
// The literal defect, asserted so a later refactor reintroducing it fails
|
|
// here rather than in a browser.
|
|
if strings.Contains(body, "body: size ? {") || strings.Contains(body, "body: { model") {
|
|
t.Error("sendImage() still passes an object literal as the fetch body")
|
|
}
|
|
}
|
|
|
|
// TestUI_PostCallsSendJSONString guards every POST in the file, not just the
|
|
// image one. The image page happened to be the only place that used api() with
|
|
// an object body, so a check scoped to it would pass while another page had the
|
|
// same defect.
|
|
func TestUI_PostCallsSendJSONString(t *testing.T) {
|
|
ui := uiSource(t)
|
|
lines := strings.Split(ui, "\n")
|
|
|
|
var bad []string
|
|
for i, l := range lines {
|
|
if !strings.Contains(l, `method: "POST"`) {
|
|
continue
|
|
}
|
|
end := i + 8
|
|
if end > len(lines) {
|
|
end = len(lines)
|
|
}
|
|
window := strings.Join(lines[i:end], "\n")
|
|
if !strings.Contains(window, "body:") {
|
|
continue // no body at all: legitimate (a bare trigger)
|
|
}
|
|
if strings.Contains(window, "JSON.stringify") || strings.Contains(window, "FormData") {
|
|
continue
|
|
}
|
|
bad = append(bad, strings.TrimSpace(l))
|
|
}
|
|
if len(bad) > 0 {
|
|
t.Errorf("%d POST call(s) send a non-string body:", len(bad))
|
|
for _, b := range bad {
|
|
if len(b) > 100 {
|
|
b = b[:100]
|
|
}
|
|
t.Errorf(" %s", b)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUI_ImagePageReportsServerErrors pins that a failed generation surfaces
|
|
// something actionable. The production bug produced a bare "400 Bad Request"
|
|
// toast; the requirement is that the gateway's own message reaches the page.
|
|
func TestUI_ImagePageReportsServerErrors(t *testing.T) {
|
|
body := sendImageSource(t)
|
|
|
|
// api() throws Error(j.error.message) on !r.ok, so appending e.message is
|
|
// what puts the explanation on screen.
|
|
if !strings.Contains(body, "e && e.message") {
|
|
t.Error("sendImage() does not surface the server's error message; a " +
|
|
"failure would show a bare status with no cause")
|
|
}
|
|
// And a success carrying no image must not look like a success.
|
|
if !strings.Contains(body, "imgNoData") {
|
|
t.Error("sendImage() does not warn when the upstream returns no image")
|
|
}
|
|
}
|
|
|
|
// sendImageSource returns the text of sendImage() only, so an assertion cannot
|
|
// be satisfied by a string that happens to appear elsewhere in the file.
|
|
func sendImageSource(t *testing.T) string {
|
|
t.Helper()
|
|
ui := uiSource(t)
|
|
i := strings.Index(ui, "async function sendImage(")
|
|
if i < 0 {
|
|
t.Fatal("sendImage() not found")
|
|
}
|
|
body := ui[i:]
|
|
if end := strings.Index(body[1:], "\n function "); end > 0 {
|
|
body = body[:end+1]
|
|
}
|
|
return body
|
|
} |