@@ -2,9 +2,11 @@ package component
22
33import (
44 "bytes"
5+ "fmt"
56 "io"
67 "mime/multipart"
78 "strings"
9+ "sync"
810 "testing"
911
1012 "github.com/projectdiscovery/retryablehttp-go"
@@ -173,3 +175,72 @@ func TestMultiPartFormComponent(t *testing.T) {
173175 require .Contains (t , string (newBody ), "username" , "unexpected body content" )
174176 require .Contains (t , string (newBody ), "testuser" , "unexpected body content" )
175177}
178+
179+ // each goroutine creates its own multipart body with a unique boundary
180+ // and parses it concurrently. before the fix this would crash with
181+ // "fatal error: concurrent map writes" because all goroutines shared
182+ // the same MultiPartForm singleton.
183+ func TestMultiPartFormConcurrentParse (t * testing.T ) {
184+ const goroutines = 20
185+ var wg sync.WaitGroup
186+ wg .Add (goroutines )
187+
188+ errs := make (chan error , goroutines )
189+
190+ for i := 0 ; i < goroutines ; i ++ {
191+ go func (id int ) {
192+ defer wg .Done ()
193+
194+ formData := & bytes.Buffer {}
195+ writer := multipart .NewWriter (formData )
196+ _ = writer .WriteField ("field" , fmt .Sprintf ("value-%d" , id ))
197+ contentType := writer .FormDataContentType ()
198+ _ = writer .Close ()
199+
200+ req , err := retryablehttp .NewRequest ("POST" , "https://example.com" , bytes .NewReader (formData .Bytes ()))
201+ if err != nil {
202+ errs <- fmt .Errorf ("goroutine %d: new request: %w" , id , err )
203+ return
204+ }
205+ req .Header .Set ("Content-Type" , contentType )
206+
207+ body := NewBody ()
208+ parsed , err := body .Parse (req )
209+ if err != nil {
210+ errs <- fmt .Errorf ("goroutine %d: parse: %w" , id , err )
211+ return
212+ }
213+ if ! parsed {
214+ errs <- fmt .Errorf ("goroutine %d: body was not parsed" , id )
215+ return
216+ }
217+
218+ _ = body .SetValue ("field" , fmt .Sprintf ("fuzzed-%d" , id ))
219+
220+ rebuilt , err := body .Rebuild ()
221+ if err != nil {
222+ errs <- fmt .Errorf ("goroutine %d: rebuild: %w" , id , err )
223+ return
224+ }
225+
226+ rebuiltBody , err := io .ReadAll (rebuilt .Body )
227+ if err != nil {
228+ errs <- fmt .Errorf ("goroutine %d: read rebuilt body: %w" , id , err )
229+ return
230+ }
231+
232+ expected := fmt .Sprintf ("fuzzed-%d" , id )
233+ if ! strings .Contains (string (rebuiltBody ), expected ) {
234+ errs <- fmt .Errorf ("goroutine %d: rebuilt body missing %q" , id , expected )
235+ return
236+ }
237+ }(i )
238+ }
239+
240+ wg .Wait ()
241+ close (errs )
242+
243+ for err := range errs {
244+ t .Error (err )
245+ }
246+ }
0 commit comments