88 "context"
99 "errors"
1010 "net/http"
11+ "strconv"
1112 "time"
1213
1314 "github.com/ethersphere/bee/v2/pkg/cac"
@@ -28,14 +29,27 @@ func (s *Service) chunkUploadStreamHandler(w http.ResponseWriter, r *http.Reques
2829 logger := s .logger .WithName ("chunks_stream" ).Build ()
2930
3031 headers := struct {
31- BatchID []byte `map:"Swarm-Postage-Batch-Id" validate:"required"`
32+ BatchID []byte `map:"Swarm-Postage-Batch-Id"` // Optional: omit if caller provides pre-signed stamps per chunk
3233 SwarmTag uint64 `map:"Swarm-Tag"`
3334 }{}
3435 if response := s .mapStructure (r .Header , & headers ); response != nil {
3536 response ("invalid header params" , logger , w )
3637 return
3738 }
3839
40+ // Fallback: read tag from query parameter (browser WebSocket can't set headers)
41+ if headers .SwarmTag == 0 {
42+ if qTag := r .URL .Query ().Get ("swarm-tag" ); qTag != "" {
43+ parsed , err := strconv .ParseUint (qTag , 10 , 64 )
44+ if err != nil {
45+ logger .Debug ("invalid swarm-tag query parameter" , "value" , qTag , "error" , err )
46+ jsonhttp .BadRequest (w , "invalid swarm-tag query parameter" )
47+ return
48+ }
49+ headers .SwarmTag = parsed
50+ }
51+ }
52+
3953 var (
4054 tag uint64
4155 err error
@@ -55,29 +69,36 @@ func (s *Service) chunkUploadStreamHandler(w http.ResponseWriter, r *http.Reques
5569 }
5670 }
5771
58- // if tag not specified use direct upload
59- // Using context.Background here because the putter's lifetime extends beyond that of the HTTP request.
60- putter , err := s .newStamperPutter (context .Background (), putterOptions {
61- BatchID : headers .BatchID ,
62- TagID : tag ,
63- Deferred : tag != 0 ,
64- })
65- if err != nil {
66- logger .Debug ("get putter failed" , "error" , err )
67- logger .Error (nil , "get putter failed" )
68- switch {
69- case errors .Is (err , errBatchUnusable ) || errors .Is (err , postage .ErrNotUsable ):
70- jsonhttp .UnprocessableEntity (w , "batch not usable yet or does not exist" )
71- case errors .Is (err , postage .ErrNotFound ):
72- jsonhttp .NotFound (w , "batch with id not found" )
73- case errors .Is (err , errInvalidPostageBatch ):
74- jsonhttp .BadRequest (w , "invalid batch id" )
75- case errors .Is (err , errUnsupportedDevNodeOperation ):
76- jsonhttp .BadRequest (w , errUnsupportedDevNodeOperation )
77- default :
78- jsonhttp .BadRequest (w , nil )
72+ // Create connection-level putter only if BatchID is provided.
73+ // If BatchID is not provided, the API caller is expected to provide
74+ // pre-signed stamps with each chunk (and is also expected to keep
75+ // track of stamp state over time).
76+ var putter storer.PutterSession
77+ if len (headers .BatchID ) > 0 {
78+ // if tag not specified use direct upload
79+ // Using context.Background here because the putter's lifetime extends beyond that of the HTTP request.
80+ putter , err = s .newStamperPutter (context .Background (), putterOptions {
81+ BatchID : headers .BatchID ,
82+ TagID : tag ,
83+ Deferred : tag != 0 ,
84+ })
85+ if err != nil {
86+ logger .Debug ("get putter failed" , "error" , err )
87+ logger .Error (nil , "get putter failed" )
88+ switch {
89+ case errors .Is (err , errBatchUnusable ) || errors .Is (err , postage .ErrNotUsable ):
90+ jsonhttp .UnprocessableEntity (w , "batch not usable yet or does not exist" )
91+ case errors .Is (err , postage .ErrNotFound ):
92+ jsonhttp .NotFound (w , "batch with id not found" )
93+ case errors .Is (err , errInvalidPostageBatch ):
94+ jsonhttp .BadRequest (w , "invalid batch id" )
95+ case errors .Is (err , errUnsupportedDevNodeOperation ):
96+ jsonhttp .BadRequest (w , errUnsupportedDevNodeOperation )
97+ default :
98+ jsonhttp .BadRequest (w , nil )
99+ }
100+ return
79101 }
80- return
81102 }
82103
83104 upgrader := websocket.Upgrader {
@@ -95,13 +116,46 @@ func (s *Service) chunkUploadStreamHandler(w http.ResponseWriter, r *http.Reques
95116 }
96117
97118 s .wsWg .Add (1 )
98- go s .handleUploadStream (logger , wsConn , putter )
119+ var decode chunkDecoder
120+ if len (headers .BatchID ) > 0 {
121+ decode = decodeChunkWithoutStamp
122+ } else {
123+ decode = decodeChunkWithStamp
124+ }
125+ go s .handleUploadStream (logger , wsConn , putter , tag , decode )
126+ }
127+
128+ // chunkDecoder extracts chunk data and optionally a stamp from a websocket message.
129+ // When BatchID is provided in headers, decodeChunkWithoutStamp is used (no stamp in message).
130+ // When BatchID is not provided, decodeChunkWithStamp is used (stamp prepended to chunk data).
131+ type chunkDecoder func (msg []byte ) (chunkData []byte , stamp * postage.Stamp , err error )
132+
133+ // decodeChunkWithoutStamp returns the message as-is (used when BatchID provided in headers).
134+ func decodeChunkWithoutStamp (msg []byte ) ([]byte , * postage.Stamp , error ) {
135+ return msg , nil , nil
136+ }
137+
138+ // decodeChunkWithStamp extracts a stamp from the first 113 bytes of the message.
139+ // Returns an error if the message is too small or the stamp is invalid.
140+ func decodeChunkWithStamp (msg []byte ) ([]byte , * postage.Stamp , error ) {
141+ if len (msg ) < postage .StampSize + swarm .SpanSize {
142+ return nil , nil , errors .New ("message too small for stamp + chunk" )
143+ }
144+
145+ stamp := & postage.Stamp {}
146+ if err := stamp .UnmarshalBinary (msg [:postage .StampSize ]); err != nil {
147+ return nil , nil , errors .New ("invalid stamp" )
148+ }
149+
150+ return msg [postage .StampSize :], stamp , nil
99151}
100152
101153func (s * Service ) handleUploadStream (
102154 logger log.Logger ,
103155 conn * websocket.Conn ,
104156 putter storer.PutterSession ,
157+ tag uint64 ,
158+ decode chunkDecoder ,
105159) {
106160 defer s .wsWg .Done ()
107161
@@ -111,11 +165,23 @@ func (s *Service) handleUploadStream(
111165 gone = make (chan struct {})
112166 err error
113167 )
168+
169+ // Cache for batch validation to avoid database lookups for every chunk
170+ // Key: batch ID, Value: stored batch info
171+ // This avoids the expensive batchStore.Get() call for each chunk
172+ batchCache := make (map [string ]* postage.Batch )
173+
114174 defer func () {
115175 cancel ()
116176 _ = conn .Close ()
117- if err = putter .Done (swarm .ZeroAddress ); err != nil {
118- logger .Error (err , "chunk upload stream: syncing chunks failed" )
177+
178+ // No cleanup needed for batch cache - it's just metadata
179+
180+ // Only call Done on connection-level putter if it exists
181+ if putter != nil {
182+ if err = putter .Done (swarm .ZeroAddress ); err != nil {
183+ logger .Error (err , "chunk upload stream: syncing chunks failed" )
184+ }
119185 }
120186 }()
121187
@@ -190,17 +256,80 @@ func (s *Service) handleUploadStream(
190256 return
191257 }
192258
193- chunk , err := cac .NewWithDataSpan (msg )
259+ // Decode the message using the appropriate decoder
260+ chunkData , stamp , err := decode (msg )
261+ if err != nil {
262+ logger .Debug ("chunk upload stream: decode failed" , "error" , err )
263+ logger .Error (nil , "chunk upload stream: " + err .Error ())
264+ sendErrorClose (websocket .CloseInternalServerErr , err .Error ())
265+ return
266+ }
267+
268+ // Determine the putter to use
269+ var (
270+ chunk swarm.Chunk
271+ chunkPutter = putter
272+ )
273+
274+ // If stamp was extracted, create a per-chunk putter
275+ if stamp != nil {
276+ batchID := stamp .BatchID ()
277+ batchIDKey := string (batchID )
278+
279+ storedBatch , exists := batchCache [batchIDKey ]
280+ if ! exists {
281+ storedBatch , err = s .batchStore .Get (batchID )
282+ if err != nil {
283+ logger .Debug ("chunk upload stream: batch validation failed" , "error" , err )
284+ logger .Error (nil , "chunk upload stream: batch validation failed" )
285+ if errors .Is (err , storage .ErrNotFound ) {
286+ sendErrorClose (websocket .CloseInternalServerErr , "batch not found" )
287+ } else {
288+ sendErrorClose (websocket .CloseInternalServerErr , "batch validation failed" )
289+ }
290+ return
291+ }
292+ batchCache [batchIDKey ] = storedBatch
293+ }
294+
295+ chunkPutter , err = s .newStampedPutterWithBatch (ctx , putterOptions {
296+ BatchID : batchID ,
297+ TagID : tag ,
298+ Deferred : tag != 0 ,
299+ }, stamp , storedBatch )
300+ if err != nil {
301+ logger .Debug ("chunk upload stream: failed to create stamped putter" , "error" , err )
302+ logger .Error (nil , "chunk upload stream: failed to create stamped putter" )
303+ switch {
304+ case errors .Is (err , errBatchUnusable ) || errors .Is (err , postage .ErrNotUsable ):
305+ sendErrorClose (websocket .CloseInternalServerErr , "batch not usable" )
306+ case errors .Is (err , postage .ErrNotFound ):
307+ sendErrorClose (websocket .CloseInternalServerErr , "batch not found" )
308+ default :
309+ sendErrorClose (websocket .CloseInternalServerErr , "stamped putter creation failed" )
310+ }
311+ return
312+ }
313+ }
314+
315+ chunk , err = cac .NewWithDataSpan (chunkData )
194316 if err != nil {
195- logger .Debug ("chunk upload stream: create chunk failed" , "error" , err )
317+ logger .Debug ("chunk upload stream: create chunk failed" , "error" , err , "chunk_size" , len ( chunkData ) )
196318 logger .Error (nil , "chunk upload stream: create chunk failed" )
319+ if chunkPutter != putter {
320+ _ = chunkPutter .Cleanup ()
321+ }
322+ sendErrorClose (websocket .CloseInternalServerErr , "invalid chunk data" )
197323 return
198324 }
199325
200- err = putter .Put (ctx , chunk )
326+ err = chunkPutter .Put (ctx , chunk )
201327 if err != nil {
202328 logger .Debug ("chunk upload stream: write chunk failed" , "address" , chunk .Address (), "error" , err )
203329 logger .Error (nil , "chunk upload stream: write chunk failed" )
330+ if chunkPutter != putter {
331+ _ = chunkPutter .Cleanup ()
332+ }
204333 switch {
205334 case errors .Is (err , postage .ErrBucketFull ):
206335 sendErrorClose (websocket .CloseInternalServerErr , "batch is overissued" )
@@ -210,6 +339,13 @@ func (s *Service) handleUploadStream(
210339 return
211340 }
212341
342+ // Clean up per-chunk putter
343+ if chunkPutter != putter {
344+ if err := chunkPutter .Done (swarm .ZeroAddress ); err != nil {
345+ logger .Error (err , "chunk upload stream: failed to finalize per-chunk putter" )
346+ }
347+ }
348+
213349 err = sendMsg (websocket .BinaryMessage , successWsMsg )
214350 if err != nil {
215351 s .logger .Debug ("chunk upload stream: sending success message failed" , "error" , err )
0 commit comments