fix: support openclaw artifact range downloads
This commit is contained in:
parent
6e00173022
commit
3932c6bd8f
@ -141,13 +141,31 @@ func (s *Server) HandleOpenClawArtifactDownload(w http.ResponseWriter, r *http.R
|
||||
if contentType == "" {
|
||||
contentType = artifactContentType(relativePath)
|
||||
}
|
||||
rangeStart, rangeEnd, partialContent, rangeOK := openClawArtifactContentRange(
|
||||
r.Header.Get("Range"),
|
||||
len(content),
|
||||
)
|
||||
if !rangeOK {
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", len(content)))
|
||||
shared.WriteJSONError(w, nil, http.StatusRequestedRangeNotSatisfiable, -32049, "invalid artifact range")
|
||||
return
|
||||
}
|
||||
body := content
|
||||
statusCode := http.StatusOK
|
||||
if partialContent {
|
||||
body = content[rangeStart : rangeEnd+1]
|
||||
statusCode = http.StatusPartialContent
|
||||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", rangeStart, rangeEnd, len(content)))
|
||||
}
|
||||
filename := filepath.Base(relativePath)
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(content)))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, strings.ReplaceAll(filename, `"`, "")))
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(content)
|
||||
w.WriteHeader(statusCode)
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
func (s *Server) readOpenClawArtifactWithRetry(
|
||||
@ -426,6 +444,52 @@ func openClawArtifactSigningSecret() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func openClawArtifactContentRange(rawRange string, contentLength int) (int, int, bool, bool) {
|
||||
if strings.TrimSpace(rawRange) == "" {
|
||||
return 0, contentLength - 1, false, true
|
||||
}
|
||||
if contentLength <= 0 {
|
||||
return 0, 0, false, false
|
||||
}
|
||||
rawRange = strings.TrimSpace(rawRange)
|
||||
if !strings.HasPrefix(rawRange, "bytes=") || strings.Contains(rawRange, ",") {
|
||||
return 0, 0, false, false
|
||||
}
|
||||
spec := strings.TrimSpace(strings.TrimPrefix(rawRange, "bytes="))
|
||||
parts := strings.SplitN(spec, "-", 2)
|
||||
if len(parts) != 2 {
|
||||
return 0, 0, false, false
|
||||
}
|
||||
startRaw := strings.TrimSpace(parts[0])
|
||||
endRaw := strings.TrimSpace(parts[1])
|
||||
if startRaw == "" {
|
||||
suffixLength, err := strconv.Atoi(endRaw)
|
||||
if err != nil || suffixLength <= 0 {
|
||||
return 0, 0, false, false
|
||||
}
|
||||
start := contentLength - suffixLength
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
return start, contentLength - 1, true, true
|
||||
}
|
||||
start, err := strconv.Atoi(startRaw)
|
||||
if err != nil || start < 0 || start >= contentLength {
|
||||
return 0, 0, false, false
|
||||
}
|
||||
end := contentLength - 1
|
||||
if endRaw != "" {
|
||||
end, err = strconv.Atoi(endRaw)
|
||||
if err != nil || end < start {
|
||||
return 0, 0, false, false
|
||||
}
|
||||
if end >= contentLength {
|
||||
end = contentLength - 1
|
||||
}
|
||||
}
|
||||
return start, end, true, true
|
||||
}
|
||||
|
||||
func artifactSHA256Matches(content []byte, expected string) bool {
|
||||
expected = strings.TrimSpace(strings.ToLower(expected))
|
||||
if expected == "" || len(expected) != 64 {
|
||||
|
||||
@ -915,6 +915,79 @@ func TestHTTPHandlerOpenClawArtifactDownloadReadsViaGateway(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPHandlerOpenClawArtifactDownloadSupportsRangeResume(t *testing.T) {
|
||||
gateway := newAcpFakeOpenClawGateway(t)
|
||||
defer gateway.Close()
|
||||
|
||||
t.Setenv("GATEWAY_RPC_URL", gateway.URL())
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-token")
|
||||
|
||||
server := NewServer()
|
||||
downloadURL := server.openClawArtifactDownloadURL(
|
||||
"thread-openclaw-artifact",
|
||||
"run-1",
|
||||
"tasks/thread-openclaw-artifact/run-1",
|
||||
"reports/final.md",
|
||||
time.Now(),
|
||||
)
|
||||
if downloadURL == "" {
|
||||
t.Fatal("expected signed download URL")
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, downloadURL, nil)
|
||||
request.Header.Set("Authorization", "Bearer bridge-token")
|
||||
request.Header.Set("Range", "bytes=6-")
|
||||
server.Handler().ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusPartialContent {
|
||||
t.Fatalf("expected 206, got %d body=%q", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if got := recorder.Body.String(); got != "report" {
|
||||
t.Fatalf("expected resumed artifact content, got %q", got)
|
||||
}
|
||||
if got := recorder.Header().Get("Accept-Ranges"); got != "bytes" {
|
||||
t.Fatalf("expected byte ranges to be advertised, got %q", got)
|
||||
}
|
||||
if got := recorder.Header().Get("Content-Range"); got != "bytes 6-11/12" {
|
||||
t.Fatalf("expected content range, got %q", got)
|
||||
}
|
||||
if got := recorder.Header().Get("Content-Length"); got != "6" {
|
||||
t.Fatalf("expected partial content length, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPHandlerOpenClawArtifactDownloadRejectsInvalidRange(t *testing.T) {
|
||||
gateway := newAcpFakeOpenClawGateway(t)
|
||||
defer gateway.Close()
|
||||
|
||||
t.Setenv("GATEWAY_RPC_URL", gateway.URL())
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-token")
|
||||
|
||||
server := NewServer()
|
||||
downloadURL := server.openClawArtifactDownloadURL(
|
||||
"thread-openclaw-artifact",
|
||||
"run-1",
|
||||
"tasks/thread-openclaw-artifact/run-1",
|
||||
"reports/final.md",
|
||||
time.Now(),
|
||||
)
|
||||
if downloadURL == "" {
|
||||
t.Fatal("expected signed download URL")
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, downloadURL, nil)
|
||||
request.Header.Set("Authorization", "Bearer bridge-token")
|
||||
request.Header.Set("Range", "bytes=99-")
|
||||
server.Handler().ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusRequestedRangeNotSatisfiable {
|
||||
t.Fatalf("expected 416, got %d body=%q", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if got := recorder.Header().Get("Content-Range"); got != "bytes */12" {
|
||||
t.Fatalf("expected unsatisfied content range, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPHandlerOpenClawArtifactDownloadRetriesTransientReadFailure(t *testing.T) {
|
||||
gateway := newAcpFakeOpenClawGateway(t)
|
||||
gateway.FailNextArtifactReads(1)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user