AlistGo/alist · warning

chunkNum invalid

Error message

chunkNum invalid

What it means

Error path in alist's ServeHTTP for a multi-range request ('Range: bytes=0-99,200-299'). Before streaming the multipart/byteranges response, the server pre-computes the encoded size via rangesMIMESize (internal/net/util.go:290); that function only fails when multipart.Writer.CreatePart cannot format the part header, i.e. when mime.FormatMediaType rejects the Content-Type (malformed media type parameters). On failure the error is sent with HTTP 416. Note a defect in this branch: unlike the other two paths there is no `return nil` after http.Error (serve.go:158), so execution continues, sets code=206, and later WriteHeader calls become superfluous, corrupting the response.

Source

Thrown at drivers/115/util.go:525

	if fileSize > 9*utils.GB { // 文件大小大于9GB时分为10000片
		if chunks, err = SplitFileByPartNum(fileSize, 10000); err != nil {
			return
		}
	}
	// 单个分片大小不能小于100KB
	if chunks[0].Size < 100*utils.KB {
		if chunks, err = SplitFileByPartSize(fileSize, 100*utils.KB); err != nil {
			return
		}
	}
	return
}

// SplitFileByPartNum splits big file into parts by the num of parts.
// Split the file with specified parts count, returns the split result when error is nil.
func SplitFileByPartNum(fileSize int64, chunkNum int) ([]oss.FileChunk, error) {
	if chunkNum <= 0 || chunkNum > 10000 {
		return nil, errors.New("chunkNum invalid")
	}

	if int64(chunkNum) > fileSize {
		return nil, errors.New("oss: chunkNum invalid")
	}

	var chunks []oss.FileChunk
	chunk := oss.FileChunk{}
	chunkN := (int64)(chunkNum)
	for i := int64(0); i < chunkN; i++ {
		chunk.Number = int(i + 1)
		chunk.Offset = i * (fileSize / chunkN)
		if i == chunkN-1 {
			chunk.Size = fileSize/chunkN + fileSize%chunkN
		} else {
			chunk.Size = fileSize / chunkN
		}
		chunks = append(chunks, chunk)

View on GitHub (pinned to 843d9dc814)

Solutions

  1. Capture the exact Content-Type being served (curl -I) and sanitize it: strip or quote parameters, or override it to a clean value like 'application/octet-stream' before the response is written.
  2. As a client, split the multi-range request into separate single-range requests — the single-range path (len(ranges)==1) does not call rangesMIMESize and avoids the failure entirely.
  3. If you control the caller of ServeHTTP, normalize w.Header().Get("Content-Type") with mime.ParseMediaType + mime.FormatMediaType (dropping bad params) before invoking it.
  4. If you maintain alist itself, add `return nil` after the http.Error call at serve.go:158 so the handler stops instead of double-writing headers.

Example fix

// before (alist-internal, serve.go): missing return corrupts the response
sendSize, err = rangesMIMESize(ranges, contentType, size)
if err != nil {
    http.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable)
}
code = http.StatusPartialContent

// after: stop after reporting the error
sendSize, err = rangesMIMESize(ranges, contentType, size)
if err != nil {
    http.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable)
    return nil
}
code = http.StatusPartialContent
Defensive patterns

Strategy: validation

Validate before calling

// Before serving, normalize the Content-Type so mime.FormatMediaType cannot fail:
func sanitizeContentType(ct string) string {
    mt, params, err := mime.ParseMediaType(ct)
    if err != nil {
        return "application/octet-stream"
    }
    clean, err := mime.FormatMediaType(mt, params)
    if err != nil {
        return "application/octet-stream"
    }
    return clean
}
// client-side: avoid multi-range requests entirely
func requestRanges(url string, ranges [][2]int64) ([]*http.Response, error) {
    // one request per range -> single-range path, rangesMIMESize never runs
    for _, rg := range ranges {
        req, _ := http.NewRequest("GET", url, nil)
        req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", rg[0], rg[1]))
        resp, err := http.DefaultClient.Do(req)
        if err != nil || resp.StatusCode >= 400 {
            return nil, fmt.Errorf("range %v failed: %v", rg, err)
        }
        _ = resp
    }
    return nil, nil
}

Prevention

When it happens

Trigger: A client sends multiple ranges in one request AND the effective Content-Type is malformed — e.g. an upstream-mounted file server returns a Content-Type with invalid parameters (unquoted special characters, bad boundary, stray semicolons), which alist copies onto w.Header() before ServeHTTP runs. ParseRange has already succeeded and sizes are valid; the failure is purely in MIME header formatting.

Common situations: Exotic download accelerators and some CLI tools (curl 7.x multi-URL, aria2 in segment mode) that batch ranges; proxying/mounting remote servers that emit non-conformal Content-Type headers; custom code that sets Content-Type manually with unquoted parameter values before calling ServeHTTP; rarely triggered because most clients send one range per request.

Related errors


AI-assisted analysis of AlistGo/alist@843d9dc814 (2026-08-15). Data as JSON: /api/errors/17803857b0381ad5. Report an issue: GitHub.