refactor(relay): move replay metadata onto request bodies
This commit is contained in:
+29
-4
@@ -29,6 +29,14 @@ type BodyStorage interface {
|
||||
NewReader() (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
// ReplayableBody is an outbound request body that can report its byte size and
|
||||
// create independent readers for transport-level retries.
|
||||
type ReplayableBody interface {
|
||||
io.Reader
|
||||
Size() int64
|
||||
NewReader() (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
// ErrStorageClosed 存储已关闭错误
|
||||
var ErrStorageClosed = fmt.Errorf("body storage is closed")
|
||||
|
||||
@@ -339,10 +347,27 @@ func CreateBodyStorageFromReader(reader io.Reader, contentLength int64, maxBytes
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// ReaderOnly wraps an io.Reader to hide io.Closer, preventing http.NewRequest
|
||||
// from type-asserting io.ReadCloser and closing the underlying BodyStorage.
|
||||
func ReaderOnly(r io.Reader) io.Reader {
|
||||
return struct{ io.Reader }{r}
|
||||
type replayableBodyReader struct {
|
||||
storage BodyStorage
|
||||
}
|
||||
|
||||
func (r replayableBodyReader) Read(p []byte) (int, error) {
|
||||
return r.storage.Read(p)
|
||||
}
|
||||
|
||||
func (r replayableBodyReader) Size() int64 {
|
||||
return r.storage.Size()
|
||||
}
|
||||
|
||||
func (r replayableBodyReader) NewReader() (io.ReadCloser, error) {
|
||||
return r.storage.NewReader()
|
||||
}
|
||||
|
||||
// NewReplayableBodyReader exposes the replay capabilities of storage without
|
||||
// exposing io.Closer. This keeps ownership of the storage lifecycle with the
|
||||
// caller instead of allowing net/http to close it as the request body.
|
||||
func NewReplayableBodyReader(storage BodyStorage) ReplayableBody {
|
||||
return replayableBodyReader{storage: storage}
|
||||
}
|
||||
|
||||
// CleanupOldCacheFiles 清理旧的缓存文件(用于启动时清理残留)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewReplayableBodyReaderKeepsStorageLifecycleWithCaller(t *testing.T) {
|
||||
payload := []byte(`{"model":"test-model","input":"hello"}`)
|
||||
storage, err := CreateBodyStorage(payload)
|
||||
require.NoError(t, err)
|
||||
defer storage.Close()
|
||||
|
||||
body := NewReplayableBodyReader(storage)
|
||||
assert.EqualValues(t, len(payload), body.Size())
|
||||
_, exposesCloser := any(body).(io.Closer)
|
||||
assert.False(t, exposesCloser, "the request body must not expose the storage closer")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "https://example.com", body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, req.Body.Close())
|
||||
|
||||
replayBody, err := body.NewReader()
|
||||
require.NoError(t, err, "closing the HTTP request body must not close the storage")
|
||||
replay, err := io.ReadAll(replayBody)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, replayBody.Close())
|
||||
assert.Equal(t, payload, replay)
|
||||
|
||||
require.NoError(t, storage.Close())
|
||||
_, err = body.NewReader()
|
||||
require.ErrorIs(t, err, ErrStorageClosed)
|
||||
}
|
||||
Reference in New Issue
Block a user