shadow1ng/fscan · error

webscan_request_body_read_failed

webscan_request_body_read_failed

Error message

webscan_request_body_read_failed: %w

What it means

DoRequest buffers the outgoing request body with io.ReadAll so it can be replayed (redirects, GMTLS fallback). If reading req.Body fails the error is wrapped as webscan_request_body_read_failed. In practice this fires for bodies that are neither in-memory nor replayable — e.g. streaming readers, network-backed bodies, or a body already consumed/closed.

Source

Thrown at webscan/lib/Eval.go:461

			i--
		}

		// 右移已使用的位,更新计数器
		cache >>= letterIdxBits
		remain--
	}

	return string(randBytes)
}

// DoRequest 执行 HTTP 请求
// session 为 nil 时回退到全局 state(兼容 CEL runtime 等无 session 场景)
func DoRequest(req *http.Request, redirect bool, session *common.ScanSession) (*Response, error) {
	// 处理请求头
	if req.Body != nil && req.Body != http.NoBody {
		body, err := io.ReadAll(req.Body)
		if err != nil {
			return nil, fmt.Errorf("%s: %w", i18n.GetText("webscan_request_body_read_failed"), err)
		}
		_ = req.Body.Close()
		req.Body = io.NopCloser(bytes.NewReader(body))
		req.GetBody = func() (io.ReadCloser, error) {
			return io.NopCloser(bytes.NewReader(body)), nil
		}
		req.ContentLength = int64(len(body))

		// 设置 Content-Length
		req.Header.Set("Content-Length", strconv.FormatInt(req.ContentLength, 10))

		// 如果未指定 Content-Type,设置默认值
		if req.Header.Get("Content-Type") == "" {
			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		}
	}

	// 执行请求

View on GitHub (pinned to 95cc12e753)

Solutions

  1. Supply the body as an in-memory type: bytes.NewReader/io.NopCloser(bytes.Buffer)
  2. Don't reuse the same *http.Request for multiple DoRequest calls — rebuild it or rely on the Set GetBody replay the library installs
  3. Check the wrapped err for the underlying read failure (connection reset, unexpected EOF)
  4. For file uploads, read the file into memory before constructing the request

Example fix

// before (unclosable streaming body)
req.Body = pipeReader
// after
b, _ := io.ReadAll(pipeReader)
req.Body = io.NopCloser(bytes.NewReader(b))
Defensive patterns

Strategy: fallback

Validate before calling

if req.GetBody == nil && req.Body != nil && req.Body != http.NoBody {
    // body may not be replayable; buffer it yourself first
}

Try / catch

resp, err := DoRequest(req, false, session)
if err != nil {
    if strings.Contains(err.Error(), i18n.GetText("webscan_request_body_read_failed")) {
        // rebuild request with in-memory body and retry once
    }
    return err
}

Prevention

When it happens

Trigger: DoRequest(req, redirect, session) where req.Body is non-nil and non-NoBody but ReadAll errors: network/piped body dropped mid-read, body previously closed, custom ReadCloser returning an error, or a content-length mismatched truncated stream.

Common situations: CEL exploit POCs sending file uploads or generated streams, reusing an *http.Request whose body was already drained by a previous DoRequest call, proxy or TLS interception cutting the stream mid-read.

Related errors


AI-assisted analysis of shadow1ng/fscan@95cc12e753 (2026-09-06). Data as JSON: /api/errors/054948ff08e8d98b. Report an issue: GitHub.