{"record":{"id":"d9c0dbe6fe4e691d","repo":"siyuan-note/siyuan","slug":"invalid-plugin-redirect-status","errorCode":null,"errorMessage":"invalid plugin redirect status","messagePattern":"invalid plugin redirect status","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/plugin_service_protocol.go","lineNumber":154,"sourceCode":"\t}\n\tknown := false\n\tfor _, variant := range PluginServiceOptions().PluginService.Variants {\n\t\tif variant.Mode == mode {\n\t\t\tknown = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !known {\n\t\treturn fmt.Errorf(\"unknown plugin service mode: %s\", mode)\n\t}\n\tswitch mode {\n\tcase PluginServiceAdmission:\n\t\tif status != 400 && status != 404 && status != 500 && status != 503 {\n\t\t\treturn fmt.Errorf(\"undeclared plugin admission status\")\n\t\t}\n\tcase PluginServiceRedirect:\n\t\tif status != 201 && (status < 300 || status > 308) {\n\t\t\treturn fmt.Errorf(\"invalid plugin redirect status\")\n\t\t}\n\tcase PluginServiceWebSocket:\n\t\tif status != 101 && status != 400 && status != 500 {\n\t\t\treturn fmt.Errorf(\"invalid plugin WebSocket status\")\n\t\t}\n\tcase PluginServiceSSE:\n\t\tif status != 200 && status != 500 {\n\t\t\treturn fmt.Errorf(\"invalid plugin SSE status\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (b *Bundle) validatePluginServiceHTTPResponse(endpoint EndpointSchema, status int, contentType string, payload []byte) error {\n\tif status < 100 || status > 999 {\n\t\treturn fmt.Errorf(\"invalid plugin service HTTP status\")\n\t}\n\tif endpoint.Method == \"HEAD\" || status < 200 || status == 204 || status == 304 {","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/plugin_service_protocol.go#L136-L172","documentation":"Redirect-mode plugin service responses must use 201 or a 3xx status in the 300-308 range; validatePluginServiceStatus rejects any other status. The kernel enforces this so redirect responses in the contract always correspond to real HTTP redirect semantics.","triggerScenarios":"Calling StreamPluginService(PluginServiceRedirect, status, serve) or ValidatePluginServiceResponse with redirect mode and a status like 200, 400, or 309 (outside 300-308 and not 201).","commonSituations":"Returning a normal success page (200) through redirect mode; using 301/308 with a non-GET method assumption error; hand-picking an unusual code such as 310 that was removed from the HTTP spec.","solutions":["Use a valid redirect status: 301, 302, 303, 304 (where applicable), 307, 308, or 201","Use 201 only when the redirect also represents resource creation semantics","If the response is not a redirect, switch the variant (e.g. PluginServiceJSON with status 200)"],"exampleFix":"// before\nStreamPluginService(PluginServiceRedirect, 200, serve)\n// after\nStreamPluginService(PluginServiceRedirect, http.StatusFound, serve) // 302","handlingStrategy":"validation","validationCode":"func validRedirectStatus(status int) bool {\n\treturn status == 201 || (status >= 300 && status <= 308)\n}","typeGuard":null,"tryCatchPattern":"defer func() {\n\tif rec := recover(); rec != nil {\n\t\tlog.Printf(\"invalid redirect status: %v\", rec)\n\t}\n}() // around StreamPluginService(PluginServiceRedirect, ...)","preventionTips":["Use net/http constants (StatusMovedPermanently, StatusFound, StatusSeeOther, StatusTemporaryRedirect, StatusPermanentRedirect)","Never reuse a 200-OK helper for redirect responses","Add table tests covering each redirect status you emit"],"tags":["go","api-contract","plugin-service","redirect","http-status"],"backgroundTag":"unexpected-http-status","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}