saloon icon indicating copy to clipboard operation
saloon copied to clipboard

Multipart boundary mismatch when merging default connector body with request body

Open PrunaCatalin opened this issue 5 months ago • 0 comments

Summary

Multipart requests work fine when the body is defined only in the request itself.
The issue appears only when a connector has a defaultBody defined as multipart and that default body is merged with the request body.

In this scenario, the HasMultipartBody trait sets the Content-Type header manually before the multipart body stream is created. However, Guzzle's MultipartStream generates its own random boundary internally, which results in a mismatch between the boundary declared in the Content-Type header and the boundary actually used in the multipart body payload.

Impact:
Many servers treat the multipart request body as empty (input: [], files: []) because the boundary in the header and body don't match.


Solution implemented locally

This is a small workaround to always generate the multipart body stream first, and then set the Content-Type header with the boundary from MultipartBodyRepository.

Example change in createPsrRequest:

public function createPsrRequest(): RequestInterface
{
    $factories = $this->factoryCollection;

    $request = $factories->requestFactory->createRequest(
        method: $this->getMethod()->value,
        uri: $this->getUri(),
    );

    foreach ($this->headers()->all() as $headerName => $headerValue) {
        $request = $request->withHeader($headerName, $headerValue);
    }

    if ($this->body() instanceof BodyRepository) {
        $request = $request->withBody($this->body()->toStream($factories->streamFactory));
    }

    /**
     * FIX WHEN DEFAULT BODY FROM CONNECTOR IS MERGED WITH REQUEST
     * Convert the body repository into a stream using the fixed boundary
     *
     * This ensures that the boundary used in the Content-Type header
     * matches the one used in the body itself.
     */
    if ($this->body() instanceof MultipartBodyRepository) {
        $boundary = $this->body()->getBoundary();

        $request = $request
            ->withBody($this->body()->toStream($factories->streamFactory))
            ->withHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary);
    }

    // Run connector and request hooks for any final changes to the PSR request.
    $request = $this->connector->handlePsrRequest($request, $this);

    return $this->request->handlePsrRequest($request, $this);
}

Thanks, PrunaCatalin

PrunaCatalin avatar Jul 23 '25 17:07 PrunaCatalin