Server replies with 200 OK while setting error
sebastien-guay opened this issue · 9 comments
The server is returning a 200 OK even if set an error. The error handling in https://github.dev/freeconf/restconf/util.go calls http.Error(w, msg, code) and I clearly see while debugging that the code is set to 401:
func handleErr(compliance ComplianceOptions, err error, r *http.Request, w http.ResponseWriter) bool {
if err == nil {
return false
}
fc.Debug.Printf("web request error [%s] %s %s", r.Method, r.URL, err.Error())
msg := err.Error()
code := fc.HttpStatusCode(err)
if !compliance.SimpleErrorResponse {
errResp := errResponse{
Type: "protocol",
Tag: decodeErrorTag(code, err),
Path: decodeErrorPath(r.RequestURI),
Message: msg,
}
var buff bytes.Buffer
fmt.Fprintf(&buff, `{"ietf-restconf:errors":{"error":[`)
json.NewEncoder(&buff).Encode(&errResp)
fmt.Fprintf(&buff, `]}}`)
msg = buff.String()
}
http.Error(w, msg, code)
return true
}
http.Error(w, msg, code)
generates the error message http: superfluous response.WriteHeader call from github.com/freeconf/restconf.handleErr (util.go:91)
This is the http.Error() function:
func Error(w ResponseWriter, error string, code int) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(code)
fmt.Fprintln(w, error)
}
The error occurs when calling w.WriteHeader(code)
. The variable wroteHeader
in the response is already set and cannot be changed.
func (w *response) WriteHeader(code int) {
if w.conn.hijacked() {
caller := relevantCaller()
w.conn.server.logf("http: response.WriteHeader on hijacked connection from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
return
}
if w.wroteHeader {
caller := relevantCaller()
w.conn.server.logf("http: superfluous response.WriteHeader call from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
return
}
.
.
.
This is caused by the sendOutput
done in freeconf/restconf/browser_handler.go at line 171 before the handleErr and line 172.
sendOutput
calls fmt.Fprintf
at line 198. This is calling WriteHeader
eventually.
I tried your example and it works as expected. I debugged your example and compared the call stack with mine and here is what I see.
With your example we execute the line err = outputSel.LastErr
in the else.
In my case we enter in if a.Input() != nil
but not the next if input, err = readInput(r, a); err != nil
.
Then we execute this last block where sendOutput
calls fmt.Fprintf
at line 198. This is calling WriteHeader
eventually and set the 200 code. Then the handleErr
is called but setting the error code is not allowed.
if outputSel := sel.Action(input); !outputSel.IsNil() && a.Output() != nil {
w.Header().Set("Content-Type", mime.TypeByExtension(".json"))
if err = sendOutput(w, outputSel, a); err != nil {
handleErr(err, r, w)
return
}
case "POST":
if meta.IsAction(sel.Meta()) {
// RPC
a := sel.Meta().(*meta.Rpc)
var input node.Node
if a.Input() != nil {
if input, err = readInput(r, a); err != nil {
handleErr(err, r, w)
return
}
}
if outputSel := sel.Action(input); !outputSel.IsNil() && a.Output() != nil {
w.Header().Set("Content-Type", mime.TypeByExtension(".json"))
if err = sendOutput(w, outputSel, a); err != nil {
handleErr(err, r, w)
return
}
} else {
err = outputSel.LastErr
}
} else {
// CRUD - Insert
payload = nodeutil.ReadJSONIO(r.Body)
err = sel.InsertFrom(payload).LastErr
}
Now I'm trying to understand why we don't have the same call stack.
The difference with my request to the server is that my curl command has data specified with the "-d" option. That is why we have a different call stack and it seems we have an issue in that case.
I'm looking at the code and I think this block should be in the if a.Input() != nil
:
if outputSel := sel.Action(input); !outputSel.IsNil() && a.Output() != nil {
w.Header().Set("Content-Type", mime.TypeByExtension(".json"))
if err = sendOutput(w, outputSel, a); err != nil {
handleErr(err, r, w)
return
}
sel.Action(input)
uses input
which is retrieved above by if input, err = readInput(r, a); err != ni
Currenytly if outputSel := sel.Action(input); !outputSel.IsNil() && a.Output() != nil
and if a.Input() != nil
are at the same level and they should not.
If i'm reading you correctly, we need to check outputSel.LastErr
before going into that block where we potentially starting writing data in response body. I'll have to verify this later this evening but it's a good theory.
BTW: Even before this, I wanted get rid of LastErr
and just return selection and err like normal go code. the poor decision to use LastErr
thing dates back before I knew better.
Just calling handleErr(outputSel.LastErr, r, w)
is sending 401 and the response body correctly. sendoutput
does not seem to be required.
just pushed the fix to master. I was able to verified origin error and then verify it disappears w/the fix. thanks for reporting!
…
On Thu, Mar 23, 2023 at 12:49 PM sguay @.> wrote: Just calling handleErr(outputSel.LastErr, r, w) is sending 401 and the response body correctly. sendoutput does not seem to be required. — Reply to this email directly, view it on GitHub <#27 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAACA7XDXI3OVHQOJ3OD7ODW5R5H7ANCNFSM6AAAAAAWEGTD3U . You are receiving this because you commented.Message ID: @.>
Thanks a lot!