Skip to content

Releases: OtezVikentiy/symfony-jsonrpc-api-bundle

5.2 - File uploads over multipart/form-data

Choose a tag to compare

@OtezVikentiy OtezVikentiy released this 17 Aug 20:41

JSON-RPC methods can now receive file uploads over multipart/form-data, alongside their structured parameters, in a single call. Opt-in and off by default; with the feature off, behaviour is identical to 5.1.

Contributed by @tacman (#9), in the shape agreed in #8. Thank you.

How it works

  • Turn it on in two places: multipart.enabled: true for the application, and acceptsMultipart: true on the method's #[JsonRPCAPI] attribute. A multipart call to a method without the flag is -32600.
  • The request carries a jsonrpc form field with the full JSON-RPC request object (scalar params included), plus one part per file, the part name being the parameter name. Declare the parameter as Symfony\Component\HttpFoundation\File\UploadedFile and hydration does the rest.
  • File validation is Symfony's own: a declared UploadedFile compiles to Assert\Type followed by Assert\File, so multipart.max_file_bytes (Symfony size notation, e.g. '10Mi') and every PHP upload error come back as -32602 naming the field.
  • The call logger records a file as {originalName, size, mimeType} metadata, never its content. The OpenAPI generator publishes a multipart/form-data request body for flagged methods.

Security

multipart/form-data is a CORS "simple request", so enabling it reopens - for the flagged methods only - the CSRF vector the mandatory Content-Type: application/json closed in 5.0. If your app authenticates with cookies, read the "before you switch it on" section of the docs first.

Limits

Batch stays JSON-only, files at the top level of params only, POST only.

Full docs: docs/multipart.md. Changelog: CHANGELOG.md.

Backwards compatibility: none required. With multipart.enabled: false (the default), nothing changes.

5.0 — Specification Conformance

Choose a tag to compare

@OtezVikentiy OtezVikentiy released this 07 Aug 23:11

This release began with a permanent test suite written against the letter of the JSON-RPC 2.0 spec. It found seventeen places where the bundle's behaviour diverged from the protocol; sixteen are fixed here, and the seventeenth is documented as a known limitation rather than quietly left out.

Nearly everything here is BC-breaking, and it could not be otherwise: correcting a deviation from the specification changes observable behaviour by definition. There are twenty-three items that need your attention, each covered in the upgrade guide in a "what it was → what it is → what breaks → what to do" format.

Start with the guide rather than with these notes. What follows is only the part worth scheduling the upgrade for.


Security

Four things that did not work the way you probably assumed in 4.x.

Private response DTO fields were reaching clients. Serialisation read properties straight off the object through Reflection, so a private field — a password hash, an internal token — went into the payload along with everything else. What reaches the JSON now is what the class exposes: a property with a public getter, or a public property. A private one with no public getter never leaves. The same is fixed in JsonRpcRequest::toArray(), which the documentation recommends for logging requests: it was exporting private fields into the log.

Logs were written without masking. logging.masking.key_patterns defaulted to an empty list and max_body_length to zero, so a single logging.enabled: true was enough to write complete request bodies, passwords included, and the operator was expected to work out the pattern list on their own. Masking now works out of the box: twenty-nine patterns and bodies truncated to 8192 characters.

Form-encoded requests were accepted. Content-Type was not checked at all, and form-encoded is a "simple request" under the CORS specification: a third-party HTML form could call your RPC methods as the logged-in user with no preflight. Requests with a body must now carry Content-Type: application/json.

A cyclic graph in a response killed the worker. A bidirectional relation (order → user → their orders) recursed until the stack overflowed and the process segfaulted — no response, no log entry, and nothing to catch, because a stack overflow is not an exception. It is now -32603 with a well-formed JSON-RPC response.

Worth reading separately: SECURITY.md now carries a "Known trade-offs" section — deliberate decisions that are better read in advance than discovered.


Protocol

Beyond the above, these are now aligned with the specification:

  • Batches are detected by container shape, not by the first element. An invalid first element used to drop the whole array back to single-request handling, and every valid call further along was lost without a trace.
  • A Notification is the absence of id, not null. {"id": null} is a full request and gets a response; a Notification never gets one, not even an error.
  • By-position parameters (params as an array) work. That is section 4.2 of the specification, and it worked in no form at all — including the canonical subtract example with [42, 23].
  • Role denials are -32000 in a JSON-RPC error object with HTTP 200, rather than HTTP 403 carrying a bare string that broke the structure of a batch response.
  • Error codes outside the permitted ranges are normalised to -32603.
  • CORS preflight (OPTIONS) is handled by the bundle — an external CORS bundle or reverse proxy is no longer required.
  • Scalars from a query string are read as the declared type. A GET request has no body, so its payload comes from the query string, which carries no types by nature. ?params[id]=5 therefore reaches an int $id field and ?params[active]=true reaches a bool. Strictness on the JSON branch is unchanged: JSON has types, so "42" where 42 belongs stays a client error. Only unambiguous representations are read — ?params[id]=abc is still -32602.

What will break for you

The full list is in the guide. The most likely:

What Where to look
Clients not setting Content-Type: application/json item 1
Clients sending numbers as strings ("42" instead of 42) item 2
Response DTOs with fields that have no getters — those disappear item 3
An application service type-hinted as Container — will not compile item 17
Logging enabled — placeholders appear in the log item 18
cors_strict left in the config — the application will not boot item 14

There is deliberately no replacement for two of these: the container alias (inject the services you need directly) and cors_strict (the legacy comma-joined mode no longer exists at any setting).


Requirements

  • PHP 8.2 – 8.5
  • Symfony ^6.4 || ^7.0 || ^8.0

The manifest declares upper bounds instead of open-ended >=, and all twelve Symfony components the source imports, plus psr/log, directly rather than transitively. The bounds state what CI verifies, not what happens to install. doctrine/annotations is gone — nothing relied on it.


Infrastructure

CI now exists and runs: PHP 8.2/8.3/8.4/8.5 against Symfony 6.4/7.x/8.x - ten combinations, minus Symfony 8 below PHP 8.4, where the combination cannot exist. Plus a lowest-dependency job, a coverage floor, PHPStan level 9, PHP-CS-Fixer, a weekly composer audit, and a non-blocking Symfony 9 canary.

Static analysis was added for the first time: PHPStan level 9 found 28 defects, most of them paths that crash rather than report.

Thirty classes are marked @internal. That is not cosmetic — it is what buys the freedom to rework internals inside 5.x rather than waiting for the next major. The public contract is the extension points (ApiMethodInterface, the processors, PlainResponseInterface, PartialRequestInterface), JsonRpcRequest, BaseRequest, BaseResponse, JRPCException, the #[JsonRPCAPI] attribute, the logging interfaces and the configuration keys.


Known limitation

An id above PHP_INT_MAX is not echoed back byte for byte: json_decode() turns it into a lossy float. This is the seventeenth deviation — fixing it means parsing the id separately from the rest of the payload, which is beyond the scope of this release. It is marked explicitly in the test suite rather than left unsaid.


Installation

composer require otezvikentiy/json-rpc-api:^5.0

Run your tests immediately after upgrading and work through the checklist — thirteen items.

Full list of changes: CHANGELOG.md, with a Russian version alongside it at CHANGELOG.ru.md. Both links point at master: the copy pinned to this tag is Russian only, because the translation landed after the release.