Skip to content

Multipart boundary mismatch when merging default connector body with request body #512

@PrunaCatalin

Description

@PrunaCatalin

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions