freeconf/restconf

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!