snail007/goproxy · error

http decoder data err:%s

Error message

http decoder data err:%s

What it means

NewHTTPRequest splits the request line with Sscanf "%s%s" into Method and hostOrURL. If either token comes back empty — i.e. the first line does not contain two whitespace-separated tokens like "GET /path" or "CONNECT host:port" — it throws "http decoder data err:%s" with the first 50 bytes, closes the connection, and returns.

Source

Thrown at utils/structs.go:263

	}
	len, err = (*inConn).Read(buf[:])
	if err != nil {
		if err != io.EOF {
			err = fmt.Errorf("http decoder read err:%s", err)
		}
		CloseConn(inConn)
		return
	}
	req.HeadBuf = buf[:len]
	index := bytes.IndexByte(req.HeadBuf, '\n')
	if index == -1 {
		err = fmt.Errorf("http decoder data line err:%s", string(req.HeadBuf)[:50])
		CloseConn(inConn)
		return
	}
	fmt.Sscanf(string(req.HeadBuf[:index]), "%s%s", &req.Method, &req.hostOrURL)
	if req.Method == "" || req.hostOrURL == "" {
		err = fmt.Errorf("http decoder data err:%s", string(req.HeadBuf)[:50])
		CloseConn(inConn)
		return
	}
	req.Method = strings.ToUpper(req.Method)
	req.isBasicAuth = isBasicAuth
	req.basicAuth = basicAuth
	log.Printf("%s:%s", req.Method, req.hostOrURL)

	if req.IsHTTPS() {
		err = req.HTTPS()
	} else {
		err = req.HTTP()
	}
	return
}
func (req *HTTPRequest) HTTP() (err error) {
	if req.isBasicAuth {
		err = req.BasicAuth()

View on GitHub (pinned to e6d6a821db)

Solutions

  1. Look at the 50-byte payload in the error to identify the sender; port scanners and health checkers are common — filter them or point health checks at a dedicated plain endpoint.
  2. Verify the client sends a well-formed request line: METHOD SP request-target SP HTTP-version CRLF.
  3. If you control the client, test with `curl -x http://proxy:port http://example.com/` to confirm a valid request line reaches the proxy.
  4. Consider stricter parsing: parse with http.ReadRequest(bufio.NewReader(conn)) to get standards-compliant errors instead of Sscanf.

Example fix

// before
fmt.Sscanf(string(req.HeadBuf[:index]), "%s%s", &req.Method, &req.hostOrURL)
if req.Method == "" || req.hostOrURL == "" { ... }

// after
httpReq, perr := http.ReadRequest(bufio.NewReader(bytes.NewReader(req.HeadBuf)))
if perr != nil {
	err = fmt.Errorf("http decoder data err:%s", perr)
	return
}
req.Method, req.hostOrURL = httpReq.Method, httpReq.URL.String()
Defensive patterns

Strategy: validation

Validate before calling

// validate the request line shape before invoking the proxy pipeline
line := string(bytes.TrimRight(reqLine, "\r\n"))
parts := strings.Fields(line)
if len(parts) != 3 || !isKnownMethod(parts[0]) {
	return fmt.Errorf("malformed request line %q", line)
}
// then call utils.NewHTTPRequest(...)

Type guard

func isKnownMethod(m string) bool {
	switch strings.ToUpper(m) {
	case "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH", "CONNECT", "TRACE":
		return true
	}
	return false
}

Try / catch

_, err := utils.NewHTTPRequest(conn, bufSize, isAuth, auth)
if err != nil {
	var opErr *net.OpError
	if strings.HasPrefix(err.Error(), "http decoder data err") {
		log.Printf("unparseable request line from %s: %v", conn.RemoteAddr(), err)
	}
	_ = opErr
}

Prevention

When it happens

Trigger: Calling NewHTTPRequest when the request line has a method but no target (e.g. "GET\r\n"), an empty first line followed by headers only, or malformed request lines from non-HTTP clients that nevertheless contain a newline.

Common situations: Health checks or port scanners sending bare CRLFs or partial payloads; custom scripts sending incomplete HTTP; HTTP/2 preface ("PRI * HTTP/2.0" would actually parse, but binary-prefixed data may not); proxies in front sending garbled request lines.

Related errors


AI-assisted analysis of snail007/goproxy@e6d6a821db (2026-09-03). Data as JSON: /api/errors/eadc69c51f7feb94. Report an issue: GitHub.