The JSON:API OpenAPI schema generator currently includes relationship fields in the `attributes` object of a resource.
For a resource that has both attributes (e.g., `name`) and a relationship (e.g., `relatedDummy`), the generated OpenAPI definition for the JSON:API `attributes` block uses a `$ref` that points to the full resource schema. This causes all properties—including relationships—to be listed as attributes, which is incorrect per JSON:API: the `attributes` member should only contain scalar/attribute properties; relationships belong in the separate `relationships` member.

**Observed behavior**
OpenAPI output for a JSON:API resource’s data item includes:

```yaml
data:
  properties:
    attributes:
      $ref: '#/definitions/Dummy'
```

This pulls in every property defined on `Dummy`, including relationships. Consequently, Swagger UI and other tooling show relationship fields as if they were regular attributes.

**Expected behavior**
The `attributes` object should define its `properties` in-line, listing only non-relationship, attribute fields (with `type: object`). Relationships should remain in the `relationships` section. For example, `attributes` should be an object with properties for `name`, `_id`, etc., but not for `relatedDummy`. The relationships block should still contain `relatedDummy`.

**Impact**
- OpenAPI clients and documentation generators see incorrect schemas, leading to confusion and potential implementation errors.
- Developers relying on the JSON:API OpenAPI output cannot trust the `attributes` block to accurately describe the payload.

**Desired fix**
Update the JSON:API schema generation logic so that when building the `attributes` schema for the OpenAPI representation, only the actual attribute properties (not relationships) are included. The `attributes` entry should not be a `$ref` to the full resource definition; it should instead directly define its own properties based on the non-relationship fields of the resource.

fix(jsonapi): exclude relations from openapi attributes schema (#8313)


**Title:** Custom `openapiContext` on properties is ignored when generating JSON Schema / OpenAPI output

**Description:**
When a property is annotated with `#[ApiProperty(openapiContext: [...])]` the explicit OpenAPI schema definition is being overridden by automatic type inference during JSON Schema generation. This is most noticeable with array properties, where the inferred schema incorrectly replaces the user-provided structure.

**Steps to reproduce:**
1. Define a resource class with a property that has both a PHP type (e.g., `array`, `object`, a typed collection, etc.) and a custom `openapiContext` in its `ApiProperty` attribute.
2. Inspect the generated OpenAPI documentation (or the JSON Schema for that property).
3. Observe that the schema does not match the `openapiContext`; instead, a schema derived from the PHP type appears.

**Expected behavior:**
If a property explicitly specifies an `openapiContext`, the automatic type-inference logic should not overwrite or replace any part of the schema. The presence of a user-defined `openapiContext` should signal that the developer intends to control the schema structure, so the generated JSON Schema must respect that intent and not be overridden by the inferred type.

**Impact:**
This leads to incorrect OpenAPI specifications (e.g., wrong `type`, missing nested `properties`, or mismatched constraints), which can break client code generation and documentation tools. It makes it impossible to fully customise the schema of array properties when they also have a PHP type.

**Acceptance criteria:**
- When a property has a non-null `openapiContext`, the schema generation should not (re)set the type or structure based on PHP type inference.
- The fix should not break properties that *do not* have `openapiContext` – the existing type inference must continue to work as before.
- The change must be safe for all property types (scalar, object, array, etc.).

fix(metadata): wrong schema generated if openapicontext set on array (#6431)


**Title:** GraphQL normalization crashes on circular references with “Expected data to be an array.”

**Description:**
When a GraphQL query includes fields that traverse bidirectional relationships (e.g., a `Customer` → `Addresses` → `Owner` cycle), the request crashes internally. The default Symfony serializer circular reference handler returns an IRI string for the already‑visited object, but the GraphQL serialization layer expects an array for each related object and throws an `UnexpectedValueException` with the message `Expected data to be an array.` This makes any GraphQL query that touches such a cycle completely unusable.

**Expected behavior:**
The GraphQL normalizers should gracefully handle the string result produced by the default circular reference handler. Instead of crashing, they should produce an identifier‑only node (containing the resource class and its identifiers) so that the GraphQL response structure remains valid and the parent field’s expected shape is preserved.

**Impact:**
Crashes all GraphQL queries that encounter circular references in entity relationships, forcing workarounds or custom circular reference handlers that are not needed in other formats.

fix(graphql): return identifier-only node on circular reference (#8239)


Bug: explicitly assigned operation names are overwritten when ApiResource operations are processed

When defining an API resource with PHP attributes and assigning a custom name to each operation, the metadata machinery replaces the explicit names with auto‑generated names. The result is that operations cannot be reached by the name the developer assigned; route generation, link headers, and anything else relying on operation names break.

Steps to reproduce:
- Create an ApiResource with a few operations, each given a distinct `name` parameter.
- For example, a `PasswordResource` with two `Post` operations (`password_reset`, `password_set`) and one `Get` operation (`password_reset_token`), all with different URI templates and a shared route prefix.
- After the resource metadata is compiled, inspect the names of the operations in the resulting `Operations` collection.

Expected behaviour:
- The names provided in the configuration are preserved verbatim in the operation metadata, because they are already unique within the resource.

Actual behaviour:
- The explicit names are discarded. The operations end up with names generated from the URI template or short name (e.g. `_api_PasswordResource_post`), or sometimes the name becomes empty. The developer‑supplied names are completely lost.

Impact:
- It is impossible to reliably reference operations by name when more than one operation is defined on the same resource. This makes features that depend on named operations (like named links, custom route references, or documentation generators) unusable, and forces workarounds that rely on auto‑generated naming patterns.

The only case where a user‑supplied name should be overwritten is when there is an actual name collision among the operations of the same resource. As long as all names are unique, the metadata factory should keep them exactly as specified.

fix(metadata): do not override name fixes #5235 (#5237)


**Subject:** BackwardCompatibleSchemaFactory does not handle `type` arrays in draft‑4 compatibility mode

---

### Description

When a JSON Schema property declares more than one allowed type (e.g. `"type": ["number", "null"]`) and the schema is generated with draft‑4 backward compatibility enabled (`BackwardCompatibleSchemaFactory::SCHEMA_DRAFT4_VERSION = true`), the `exclusiveMinimum` and `exclusiveMaximum` constraints are not correctly transformed for draft‑4.

### Steps to reproduce

1. Define an API resource property whose JSON Schema has `"type": ["number", "null"]` (or any other array containing `"integer"` or `"number"`) and also declares `exclusiveMinimum` and/or `exclusiveMaximum`.
2. Generate the JSON Schema with the `SCHEMA_DRAFT4_VERSION` serializer context flag set to true.
3. Inspect the output schema.

### Expected behavior

- When at least one element of the `type` array is `"integer"` or `"number"`, the existing draft‑4 compatibility rules should apply:
  - `exclusiveMinimum` should be converted to `minimum` and a boolean `exclusiveMinimum: true` (and analogously for `exclusiveMaximum`).
- The transformation is identical to what already happens when `type` is a single string `"integer"` or `"number"`.

### Actual behavior

- The compatibility transformation is skipped for properties where `type` is an array, because the current check (`in_array($property['type'], ['integer', 'number'])`) only succeeds when the entire value is a string containing `'integer'` or `'number'`. Arrays containing those values are not matched, so the constraints remain in their draft‑6+ form.

### Additional constraints & edge cases

1. **Only numeric types matter**: If the `type` array is `["string", "null"]` (no numeric component), `exclusiveMinimum`/`exclusiveMaximum` should be left untouched exactly as before the change.
2. **Single‑type behavior unchanged**: When `type` is a plain string `"integer"` or `"number"`, the transformation must still work as it did.
3. **Flag‑off behavior unchanged**: When the draft‑4 flag is absent or false, no draft‑4 adjustments should be made regardless of the `type` value.
4. **Null handling**: A `type` array containing `"null"` alongside a numeric type (e.g. `["number", "null"]`) must still trigger the numeric transformation. The presence of `null` does not negate the numeric handling.
5. **`exclusiveMinimum`/`exclusiveMaximum` presence**: The transformation should only be applied when the respective field actually exists on the property. This matches existing behavior.
6. **All definitions**: This logic applies to every definition and every property in the generated JSON Schema.

fix(jsonschema): multiple type support (draft4) (#6171)


**Title:** DateTime attributes on resources using `entityClass` state option fail to serialize under JSON-LD

**Description**
When an API resource is configured with `stateOptions` pointing to a Doctrine entity via `entityClass` and that entity contains a `DateTimeInterface` property, requesting the resource with `Accept: application/ld+json` either returns a 500 error or omits the DateTime field from the output. The field should be present and serialized as a normal ISO 8601 string, just as it would be when the entity itself is the API resource class without using `entityClass`. Instead, the serializer appears to treat the DateTime value as a resource relation, attempting to generate an IRI for a plain DateTime object (e.g., `Attempted to call an undefined method “getId”` or similar IRI converter error).

**Steps to reproduce**
1. Create a Doctrine ORM entity with a `$start` property of type `DateTimeInterface`.
2. Define a separate API resource class (not the entity) with `#[ApiResource]` and `stateOptions: new Options(entityClass: MyEntity::class)`, exposing at least a `Get` operation.
3. Persist an entity instance with a DateTime value.
4. Send a GET request to the corresponding endpoint with `Accept: application/ld+json`.

**Observed behavior**
- Either a 500 Internal Server Error is thrown, with logs indicating an attempt to resolve an IRI for a `DateTime` object,
- or the `start` field is silently missing from the JSON-LD response.

**Expected behavior**
The `start` field should appear in the JSON-LD response as a correctly formatted date string (e.g., `"2024-12-01T10:30:00+00:00"`), not as a missing field or server error. Non-resource fields like DateTime values must not be forced through resource-relation normalization.

**Impact**
This prevents using `entityClass` to decouple API resource definitions from entities whenever the entity contains any DateTime (or similarly non-resource) properties. Workarounds like manually redefining the property or avoiding `entityClass` are impractical.

fix(serializer): no forced resource class relation (#5542)


**Request: Allow disabling the name converter in Laravel integration**

When the `api-platform.name_converter` configuration is set to `null` (to disable any property name conversion), the serializer layer correctly uses the raw property names without conversion. However, nested relation handling still internally forces a CamelCase-to-snake_case conversion for relation method names, causing POST/PUT requests with nested relations using the native snake_case keys to fail — relations are not resolved because the wrong name is derived.

**Acceptance criteria**

1. Setting `api-platform.name_converter` to `null` disables all name conversions in the Laravel integration, including the mapping between relation methods and JSON property names.
2. When `name_converter` is `null`, a request body that uses snake_case property names matching the database columns (e.g., `shopping_cart` for a `belongsTo` relation method `shoppingCart`) is correctly recognized and processed, allowing successful creation of resources with nested relations.
3. The configured name converter (including `null`) is respected identically for top-level attributes and for nested relation keys.
4. No BC break for the default behavior: when `name_converter` is not explicitly set to `null`, the existing CamelCase↔snake_case conversion continues to work as before.

**Context**

Laravel models often use camelCase relation method names while database columns use snake_case. The default `SnakeCaseToCamelCaseNameConverter` handles this automatically. However, some projects prefer to keep everything snake_case and want to disable conversion entirely, both for attributes and relation names.

test(laravel): test to change the name converter


```
Currently, the Elasticsearch data providers (in both `Core/Bridge/Elasticsearch/DataProvider` and `Elasticsearch/State`) unconditionally include a `type` parameter when building search and get requests. Starting with Elasticsearch 7, mapping types have been removed, and including this parameter causes errors from the server.

Change the code so the `type` parameter is only included in requests when the connected Elasticsearch version is earlier than 7. The Elasticsearch client version string is available as `Elasticsearch\Client::VERSION`. Make sure the fix is applied consistently to all providers that construct Elasticsearch requests, without duplicating the check unnecessarily.
```

fix(elasticsearch): verify whether mapping type is supported (#4726)


**Title**: JSON‑LD input schemas incorrectly require `@id` and `@type`

**Description**
When generating a JSON‑LD input schema (i.e., `type = Schema::TYPE_INPUT`, format `jsonld`), the resulting JSON Schema marks the `@id` and `@type` properties as required. Input schemas describe payloads that clients send; they should not force JSON‑LD‑only identifier or type fields, which are server‑side concerns. Output schemas must continue to include `@id` and `@type` as required for JSON‑LD compliance.

**Expected behavior**
- For **output** (`Schema::TYPE_OUTPUT`) with format `jsonld`: the schema must include `"required": ["@id", "@type"]`.
- For **input** (`Schema::TYPE_INPUT`) with format `jsonld`: the schema must **not** require `@id` or `@type`. Essentially, the generated JSON Schema should be a plain JSON Schema (as if format were `json`) rather than a JSON‑LD‑specific one. The rest of the input schema content should remain unchanged.

**Additional context**
- The Hydra‑specific `SchemaFactory` currently has separate base schemas for output vs. input; the input base schema incorrectly inherits the `required` constraint from the output base.
- The fix should ensure that when the schema type is `input`, the schema generation path treats the format as plain JSON, so the Hydra‑specific JSON‑LD layer is skipped and no JSON‑LD required fields appear.
- No changes to actual API behavior or serialization are needed; this is purely about the JSON Schema artifacts that document the API.

fix(jsonschema/jsonld): make `@id` and `@type` properties required only in the JSON-LD schema for output (#7397)


When using a `QueryParameter` attribute with a property placeholder, e.g. `key: 'sort[:property]'`, and applying a filter such as `OrderFilter`, the filter queries the database using the raw placeholder string instead of resolving it to the actual property name. This causes the order to have no effect or to fail silently, because the SQL `ORDER BY sort[:property]` is generated instead of `ORDER BY is_active`.

**Steps to reproduce**
1. Define an API resource with a query parameter that uses a property placeholder and specifies the list of allowed properties.
   Example: `#[QueryParameter(key: 'sort[:property]', filter: OrderFilter::class, properties: ['isActive'])]`
2. Send a request to the collection endpoint with `?sort[isActive]=asc`.
3. Observe that the results are not ordered by the `is_active` column.

**Expected behavior**
The filter should use the actual property name (e.g. `isActive`, which maps to the column `is_active`) when constructing the database query, not the placeholder key from the attribute.

**Additional context**
- This affects any filter that relies on the property name passed through the `Parameter` object to build its query.
- The placeholder mechanism works correctly for other parts of the query parameter resolution, but the filter extension that invokes the filter does not forward the resolved property.
- The fix should ensure that when a filter is applied, the `Parameter` instance passed to the filter’s `apply` method has its key set to the resolved property name (the value stored in the parameter’s extra properties under `_query_property`) if it exists, falling back to the original key otherwise.

fix(laravel): filter should query correct property


**Title:** `validator.serialize_payload_fields` does not work when set to `null`

**Description:**
The configuration option `validator.serialize_payload_fields` (exposed in the Symfony bundle as `serialize_payload_fields` under the `validator` key) accepts three kinds of values:

- `[]` (default): no payload fields are included in serialized constraint violations
- an array of field **names**: only those payload fields are included
- `null`: **all** payload fields present on the constraint should be included

The last case (`null`) is not working. When the option is set to `null`, the serializers (both the Hydra and Problem normalizers) behave as if it were `[]` and drop every payload field. As a result, consumers who want to see the full constraint payload (e.g., severity, custom metadata) in error responses cannot get it, even after correctly setting the option to `null`.

**Expected behaviour / acceptance criteria:**
- When `serialize_payload_fields` is `null`, **all** payload fields from the constraint’s payload array are included in the normalized violation output.
- When `serialize_payload_fields` is `[]` (the default), no payload fields are included.
- When it is an explicit list of field names, only those fields are included (this already works).
- The configuration-node info text in the bundle Configuration should clarify that `null` is the “serialize all” value.
- The fix must work for both Hydra (`jsonld` format) and Problem (`jsonproblem` format) error responses, as both normalizers share the same abstract base class containing the payload-handling logic.

**Affected components (pointers for investigation):**
- The common logic lives in `AbstractConstraintViolationListNormalizer` – its constructor stores the `$serializePayloadFields` argument and `getMessagesAndViolations()` decides whether/how to add the payload key.
- The Hydra and Problem concrete normalizers both pass the config value through to the abstract constructor.
- The Symfony Configuration class defines the default value and the help text for this config node.

Fix a bug preventing to serialize validator's payload (#3375)


We want to simplify the developer experience for resources that follow common conventions. Currently, if a resource class does not explicitly mark any property as an identifier (via `#[ApiProperty(identifier: true)]`), API Platform throws a runtime exception. Many APIs use a property called `id` as their primary identifier and never change it, so requiring the annotation is unnecessary boilerplate in that case.

**Acceptance criteria**

- When no property has been explicitly configured as an identifier:
  - If the resource class contains a property named `id`, then that property is automatically treated as the identifier. The system must not throw the “No identifier defined” exception.
  - If the resource class does not contain a property named `id` (and no explicit identifier exists), the existing exception must still be thrown, with its current message.
- The default-identifier behaviour must be consistent for both:
  - resolving identifiers from a resource class definition, and
  - extracting the identifier values from an actual resource instance.
- Resources that already have explicit identifiers (single or composite) must not be affected; the fallback to `id` only applies when zero identifiers are found after scanning the class metadata.
- The implementation must not introduce additional metadata or cache state beyond what the existing metadata machinery already provides.

Use "id" as default identifier if none provided


**Title:** Laravel parameter validation does not enforce JSON Schema enum constraints from filters

**Description:**
When a filter’s JSON Schema defines an `enum` property (e.g., the OrderFilter schema only allows `"asc"` and `"desc"`), the generated Laravel validation rules ignore that restriction entirely. This means invalid values pass without a 422 response, leading to unpredictable query behavior.

**Steps to reproduce:**
- Define a resource with an OrderFilter parameter (key `order[:property]` or a specific property).
- Send a request with an invalid sort direction: `?order[name]=something`.
- Expected: a 422 validation error.
- Actual: request succeeds (e.g., returns results, possibly ignoring the bad value).

**Acceptance criteria:**
1. If a parameter’s schema contains an `enum` list, the generated Laravel validation rules must include a corresponding `in` rule (e.g., `Rule::in($schema['enum'])`).
2. This must work for parameters that use the `:property` placeholder pattern as well. The current validation key mapping does not correctly translate such keys into Laravel’s wildcard array validation syntax. The enum rule should be applied to each entry of the array-like parameter (i.e., validate each `order.*` value).
3. OrderFilter requests with invalid directions must result in a 422 status code.
4. The existing successful validations (e.g., valid order values) must still pass.
5. The API must return a clear exception if a parameter unexpectedly lacks a defined key, instead of silently failing or misbehaving.

**Additional context:**
- The enum assertion code was previously commented out in the parameter validation factory; it needs to be enabled with the appropriate Laravel rule.
- The `ParameterValidatorProvider` currently does not handle placeholder keys correctly when assigning rules; the mapping from `order[:property]` to a validation key must use the `.*` pattern (see Laravel’s “validating arrays” wildcard rule assignment).
- The `Parameter` metadata class may need its type hints relaxed to accept non-Symfony rules (Laravel rule arrays/strings).

fix(laravel): validate enum schema within filter (#6615)


GraphQL-only resources with no identifier property should work correctly (#3975)

A resource attribute like:
```php
#[ApiResource(
    operations: [],
    graphQlOperations: [
        new Query(resolver: MyResolver::class, name: 'get'),
    ]
)]
class MyAction { ... }
```
where `MyAction` has no property annotated as an identifier currently fails during metadata processing or causes errors when the GraphQL query requests the `id` field. Queries that do not request `id` may also fail.

The metadata system must handle the absence of any identifier gracefully for such resources. Key points:
- The resource should be usable without any HTTP operations and without any identifier property.
- Executing a GraphQL query for the resource must succeed even when the query includes the `id` field; the framework may generate a synthetic identifier but must not require a path variable or identifier mapping.
- The custom resolver must be called and its result serialized correctly.
- The internal not-exposed operation that API Platform creates for URI generation must not rely on an `{id}` variable if no identifiers exist on the class.

fix(metadata): allow GraphQL-only resources without identifiers (#3975) (#7836)


**Title:** Deterministic ordering of paths and schemas in serialized OpenAPI documents

**Context:**
When the OpenAPI specification is generated and serialized (e.g., for the `/docs` endpoint or when exporting), the order of the `paths` entries and the `components/schemas` entries is currently non-deterministic. Running the same application with the same API resources produces OpenAPI documents that differ only in key ordering, causing unnecessary diffs in version-controlled documentation and confusion when comparing outputs.

**Desired behavior:**
The serialized OpenAPI JSON output must be deterministic:
- **Paths** (`paths`): endpoints should appear sorted alphabetically by their URL path string (natural string order).
- **Schemas** (`components/schemas`): schema definitions should appear sorted alphabetically by schema name.

These ordering guarantees apply during the normalization/serialization step that is responsible for cleaning and preparing the OpenAPI data for output (the component that recursively processes the OpenAPI document array before it is returned).

**Non-obvious edge cases / safety constraints:**
- Sorting must be **stable** and case-sensitive (standard `ksort` semantics are acceptable).
- The fix must not change the structure or content of the document, only the order of maps within those two specific sections.
- The fix should handle the internal data structure correctly: note that the `paths` entry initially contains a nested `paths` array due to the `Paths` model, and the serialization already unwraps it. Sorting should be applied to the unwrapped paths array.
- Other top-level keys (e.g., `info`, `servers`, `security`, `tags`, `externalDocs`) and other `components` sub-sections (responses, parameters, etc.) are not required to be sorted by this fix, though adding sorting there must not regress existing behavior.

**Acceptance criteria:**
1. Given an API with resources whose names yield different alphabetical ordering (e.g., "Dummy" and "Zorro") and their corresponding path segments, the generated OpenAPI document always has paths sorted like `/dummies`, `/dummies/{id}`, `/zorros`, `/zorros/{id}` (assuming standard resource path naming).
2. The schemas in `components/schemas` are sorted alphabetically by schema name.
3. No other structural or content changes occur in the output.


**Title**: Fix Symfony 6.3+ deprecation warnings in API Platform Core

When API Platform Core is used with Symfony 6.3 or later, several deprecation notices are triggered. The code should be updated to use the current Symfony APIs so that no deprecations are emitted and forward compatibility is preserved.

**Acceptance criteria**

1. **Argument resolver migration** – The argument resolver responsible for payload resolution (`PayloadArgumentResolver`) currently implements the deprecated `ArgumentValueResolverInterface`. Replace it with `ValueResolverInterface` (from `Symfony\Component\HttpKernel\Controller`).
   - The new interface no longer separates `supports()` from `resolve()`. The resolver must determine inside `resolve()` whether it should provide a value for the argument.
   - When the resolver does **not** handle the argument, it must return an empty iterable (for example, by yielding nothing, or yielding an empty array).
   - When the resolver **does** handle the argument, it must continue to yield the current request’s `data` attribute exactly as before.
   - The existing `supports()` logic can be reused to decide when to act; no semantic change in resolution behavior is allowed.

2. **Message handler updates** – Several classes in the test fixtures still implement the deprecated `MessageHandlerInterface`:
   - `PasswordResetRequestHandler`
   - `MessengerWithInputHandler` (both the document and entity variants)
   - `MessengerWithResponseHandler`
   Remove `implements MessageHandlerInterface` from these classes and apply the `#[AsMessageHandler]` attribute instead. Their `__invoke` signatures and behavior must remain unchanged; only the method of registering them as handlers should be updated to the non-deprecated approach.

3. **Email validation mode** – The test data within `PropertySchemaCollectionRestrictionTest` uses `Email::VALIDATION_MODE_LOOSE`, which is deprecated. Change the mode to `Email::VALIDATION_MODE_HTML5`. The expected schema outputs and assertions must still hold with this change.

After these changes, no Symfony 6.3+ deprecation notices related to these three areas should appear when running the test suite or when API Platform Core is used at runtime.

chore(deprecations): fix symfony deprecations (#5289)


**Title:** Support custom attributes that extend `ApiResource`

**Background**
API Platform discovers API resources by scanning PHP 8 attributes. Currently only the exact `ApiPlatform\Metadata\ApiResource` attribute is treated as a resource marker. If a user creates a custom attribute class that extends `ApiResource` (e.g., to provide opinionated defaults) and applies it to an entity, the class is not discovered as an API resource and its metadata is not processed.

**Expected behavior**
Any PHP 8 attribute whose class is or inherits from `ApiResource` should be treated identically to a direct `ApiResource` attribute. This includes:

- Resource name discovery (the class appears in the resource name collection).
- Resource metadata collection (operations, defaults, etc. are extracted from the attribute instance).

**Constraints / Notes**

- The change must work for both repeatable and non‑repeatable custom attributes.
- The existing handling of `HttpOperation` and `GraphQlOperation` subclasses must not be affected; those already use inheritance checks and should remain consistent.
- The fix should respect the same defaults/fallback logic that applies to a plain `ApiResource` attribute.
- No changes to the public `ApiResource` class signature are required.

**Acceptance criteria**

- An entity annotated with a custom attribute extending `ApiResource` (without additionally annotating it with `ApiResource` directly) is recognized as an API resource.
- The resource’s operations are built as if `ApiResource` had been used, using the custom attribute’s configuration and the default values for missing properties.

feat(metadata): use a custom Attribute extends ApiResource (#5076) (#5175)


In the IriConverter local operation cache (both Symfony and Laravel adapters), the cache key can mistakenly collide between item and collection operations when the resource argument is a class string. This can cause a previously cached collection route to be returned for a subsequent item IRI request (or vice versa), producing an incorrect IRI.

The cache key must reliably separate item and collection operations for all resource forms (string and object). Update the key composition so that both resource kind (`_s` / `_o`) and operation kind (`_c` / `_i`) are part of the key, preventing collisions between item and collection operations for the same resource class and operation name.

fix(symfony,laravel): IriConverter local cache key collision between item and collection ops (#7975)


```
Fix the GraphQL serialization stage to derive operation type from the operation metadata rather than from unreliable resolver-context flags.

The `SerializeStage` currently inspects `$context['is_mutation']`, `$context['is_subscription']`, and `$context['is_collection']` (combined with the operation interface) to decide which default payload to return when serialization is disabled on an operation.  The resolver context may not always carry those flags, and the flags can get out of sync with the actual operation.  This leads to incorrect default responses (e.g., a mutation getting a plain `null` instead of `['clientMutationId' => null]`).

The fix should **remove all reliance on those context keys** for this decision, and instead use the operation object:

- `$operation instanceof CollectionOperationInterface` determines whether the operation is a collection.
- `$operation instanceof Mutation` (use the existing GraphQL-specific class) determines whether it is a mutation.
- `$operation instanceof Subscription` determines whether it is a subscription.

The expected invariant: when `serialize` is turned off on an operation, the stage must still return the correct default structure:
- item query   → `null`
- collection query → (when pagination enabled) a paginated structure with `totalCount`, `edges`, `pageInfo`; (without pagination) `[]`
- mutation     → `['clientMutationId' => null]`
- subscription → `['clientSubscriptionId' => null]`

Locate the logic in `SerializeStage::__invoke` and update it to derive the operation category solely from the `Operation` instance, dropping the `$context` boolean checks.
```

fix(graphql): use operation FQDN instead of type


Title: Error responses incorrectly inherit serialization groups from the original operation

Description:
When an API operation (e.g., a POST endpoint) is configured with a `normalizationContext` that includes serialization groups, and that operation throws an exception handled by a custom error resource, the resulting error response is filtered by the original operation's groups. This means properties from the error resource (like `detail` from `ProblemExceptionInterface`) disappear from the response if they are not part of the operation’s groups.

The expected behavior is that the error response should be serialized using only the error resource's own normalization context, without merging the original operation's context. The error resource, not the triggering operation, defines what fields should be visible in the error body.

fix(symfony): error wrongly inherit normalization context (#6939)


**Title:** GraphQL property-level security can leak values across different objects due to serialization caching

**Description:**
When a GraphQL query returns multiple instances of a resource type that uses property-level access-control expressions referencing the current object (e.g., `#[ApiProperty(security: "object.owner == user")]`), the serialized value of the restricted property may be incorrectly shared between objects. This happens because the serializer caches normalized property results and reuses them for subsequent objects of the same type within the same request, without considering that the object changed.

**Reproduction steps**
1. Create two API resources: a *Child* resource with a property restricted via `#[ApiProperty(security: "object == null or object.owner == user")]` (the property should only be visible to the owner), and a *Parent* resource that has a relation to the Child.
2. Define GraphQL operations for both resources (e.g., `Query` and `QueryCollection`).
3. Create several Parent entities, each pointing to a distinct Child. Set at least two of the Children with different owners (e.g., one owned by the current user, one owned by another user).
4. Authenticate as one of the users and issue a single GraphQL query that fetches the list of Parents and includes the restricted property of each Child.
5. Observe the values returned for the restricted property.

**Current behavior**
The restricted property’s value is not consistently hidden or shown according to the per-object access rule. Sometimes a property value that belongs to another object’s owner “leaks” into a result where it should be `null` (or vice-versa). The behavior depends on the order in which objects are processed within the same HTTP/GraphQL request, pointing to a cross-object caching issue.

**Expected behavior**
Each object’s property should be evaluated independently against the property-level security expression. A property that is restricted by `object.owner == user` must be serialized only for objects whose owner matches the authenticated user. No property value from one object should appear in another object’s serialization.

**Impact**
This is a security-relevant data leak: a user may see property values that belong to resources they should not have access to, or may have their own data incorrectly hidden when it should be visible. It undermines the trustworthiness of property-level security in GraphQL endpoints.

fix(graphql): property security might be cached w/ different objects


**Title:** Missing support for `license.identifier` (SPDX license expression) in OpenAPI generation

**Problem**
When I configure an API license in the Symfony bundle (e.g. `api_platform.openapi.license.name` and `api_platform.openapi.license.url`) the generated OpenAPI spec is correct. However, the OpenAPI specification also supports an `identifier` field on the License Object that holds an SPDX license expression (e.g. `MIT`). The `License` model class (`ApiPlatform\OpenApi\Model\License`) already has an `identifier` property and getter, but there is no way to populate it through API Platform’s configuration. The Symfony configuration tree under `api_platform.openapi.license` only offers `name` and `url`.

**Expected behavior**
1. The Symfony configuration should accept a new `identifier` key under `api_platform.openapi.license`.
2. That value must flow through the DI container, be accepted by the OpenAPI Options object, and ultimately be passed as the third argument when the `License` object is constructed in the OpenAPI factory.
3. The resulting OpenAPI document should then include `license.identifier` with the configured SPDX expression when one is provided, while remaining fully backward-compatible (the `identifier` is optional and defaults to `null`).

**Additional context**
- The `License` constructor signature is `__construct(string $name, ?string $url = null, ?string $identifier = null)`.
- The note in the diff about the `identifier` and `url` fields being mutually exclusive should be documented in the configuration description so users know not to use both at the same time.

feat(openapi): license identifier (#7141)


# To-one relations with `inversedBy` matching parent association are not eagerly loaded

The eager-loading extension (`EagerLoadingExtension::joinRelations`) contains a guard that
intentionally skips a to-one join when the association metadata declares `inversedBy` equal to
the current parent association name. This guard exists to prevent infinite recursion on
self-referencing relations.

However, the guard currently fires for **any** to-one association where the inversed-by side
matches the parent, even when the source entity and target entity are different classes. In those
cases the relation is not self-referencing and no recursion risk exists; skipping the join simply
drops an eagerly-loaded relation, leading to extra queries when the relation is later accessed
during serialization or inspection.

## Expected behaviour

- For a self-referencing to-one association (same entity on both sides), the existing recursion
  guard should still apply: skip the join.
- For a to-one association between **different** entities, even when an `inversedBy` field points
  back to the parent association, the eager join must **still be performed** (assuming other
  rules, e.g. fetch mode, allow it).

## Steps to reproduce (conceptual)

- Define entity `A` with a to-one association `b` targeting entity `B`.
- On `B`, define a to-many (or to-one) inverse side that maps back to `A` via property `as`.
- On `A`’s `b` association, set `inversedBy: 'as'`.
- Query a collection or item of `A`; observe that `b` is not eagerly joined (the generated DQL
  lacks the left join and addSelect for `b`).

The bug is located in the `if` block inside `EagerLoadingExtension::joinRelations` that currently
checks only `inversedBy === parentAssociation` and `type & ClassMetadata::TO_ONE`.  That
condition must additionally verify that the association is self-referencing (source entity
class equals target entity class) before skipping the join.

fix: missing eager joins on to-one relationships (#5992)


**Title:** JSON Schema input generation omits `$ref` for embedded read‑only resources

**Description**
When I generate the JSON Schema for an input DTO (using `api:json-schema:generate --type input`) and that DTO contains a property whose type is an ApiResource that only exposes `GET`, the resulting schema incorrectly drops the `$ref` to the resource’s definition. Instead I see an empty or missing `items`/type specification where there should be a proper reference like `"$ref": "#/definitions/ProductCode.jsonld"`.

Concrete example: a `SaveProduct` DTO used as the `input` of a `POST` operation. `SaveProduct` has a `codes` collection of `ProductCode` objects. `ProductCode` is an ApiResource with only `#[Get]` – no `POST`/`PUT`/`PATCH`. The generated input schema for `SaveProduct` should still reference the `ProductCode.jsonld` definition, because the collection is written indirectly through the DTO even though the resource itself has no write operation.

**Expected behaviour**
- The JSON Schema for an input DTO must include `$ref` entries for every referenced ApiResource, regardless of whether that resource exposes write operations on its own.
- When the property is a collection, the schema should contain `"items": { "$ref": "..." }`.
- For non‑writable embedded references (e.g. when generating an output schema or when the embedded resource truly cannot be written), the current skipping behaviour can be preserved.

**Acceptance criteria**
1. Run `api:json-schema:generate` for a DTO class that embeds a read‑only ApiResource.
2. The `items` schema of the collection property must contain a valid `$ref` pointing to the definition of that embedded resource.
3. Normal input/output schema generation must not regress.

**Additional notes**
The issue is likely in the logic that decides whether a sub‑schema should be generated for writable contexts. Write operations can be inherited through an input DTO, so the decision should not solely depend on the embedded resource’s own operations.

fix(jsonschema): indirect resource input schema (#6001)


# Partial fetch selects too few fields when the same entity type is joined via multiple relations with different serialization groups

**Area**: Doctrine ORM eager loading (`EagerLoadingExtension`).

**What happens**:
When partial fetching is enabled on a resource (e.g. `fetchPartial: true`) and that resource has two (or more) relations to entities of the same type but with different serialization groups or serialization contexts (e.g. one relation uses `#[Context(normalizationContext: ['groups' => ...])]` and another does not), the currently generated DQL partial selects are computed per alias independently. Doctrine ORM may reuse the same object instance when two JOIN aliases refer to the same underlying row. If the partial select for one alias omits a field that the other alias needs, the entity ends up under-hydrated and the serialized response misses expected keys.

**Concrete example**:
- Resource `DummyContext` has `relatedWithSwitch` (group `context_switched`) and `relatedWithoutSwitch` (group `initial`), both targeting `DummyContextRelated`.
- The current code emits `partial … {id,contextSwitched}` for one alias and `partial … {id,initialGroups}` for the other.
- Doctrine may hydrate the `DummyContextRelated` instance from the narrower select, so `initialGroups` (or `contextSwitched`) is missing when the other relation is serialized.

**Expected behavior**:
1. For each entity class that appears in multiple aliases, compute the **union** of all readable fields across all those aliases for the current query.
2. Every alias for that same entity class must use that same merged field set, so the object instance is fully hydrated irrespective of which alias Doctrine uses.
3. The **root entity** must never be partial-selected; it must remain a full select.
4. The entire partial-fetch machinery should be guarded by a check that `Doctrine\ORM\Query\AST\PartialObjectExpression` exists, so we don’t break compatibility with older Doctrine ORM versions that lack partial-object support.
5. The *order* of SELECT columns in the generated DQL may change as a side-effect (the BDD `.feature` expectations will need updating); correctness of which columns are selected takes precedence over their relative order.

**Where to look**:
The faulty logic lives inside `EagerLoadingExtension` and its private methods `joinRelations`, `addSelect`, `addSelectOnce`. Currently partial selects are emitted eagerly during relation walking. The fix needs to defer SELECT clause assembly until after all relations have been visited, then merge field sets per entity class before calling `QueryBuilder::addSelect`.

fix(doctrine): fix partial fetch with same entity included multiple time with different fields (#7647)


**Title:** Mercure updates not published for all `ApiResource` declarations of an entity

**Description:**
When an entity class carries multiple `#[ApiResource]` attributes and Mercure is enabled on more than one of them, only a single update is actually sent per entity lifecycle event (create/update/delete). For example, an entity that has both an “admin” resource and a “public” resource, each with its own serialization groups and IRI, will cause updates to be published only for the first resource encountered. The remaining resource declarations silently receive no Mercure update.

The root cause is that the internal storage of pending updates (`SplObjectStorage`) is keyed by entity instance, which collapses multiple resource definitions for the same entity into a single entry. Additionally, normalization‑context and IRI resolution currently do not use the operation of the specific resource being published, so even the single update may use the wrong context.

**Expected behaviour:**
For every resource in the entity’s metadata collection that has Mercure enabled, a separate update should be queued and later published. Each update must:
- use the normalization context from the corresponding operation,
- resolve the IRI for that operation (including when building deletion payloads that contain only `@id` and `@type`),
- and be delivered to the Mercure hub with the correct topic set.

The fix must correctly handle the `deletedObjects` case as well, where a stub object is stored before the entity is actually removed.

**Constraints / safety:**
- Existing tests for single‑resource entities and GraphQL subscriptions must continue to pass.
- Expression‑language evaluation of the Mercure options must still be supported per resource.
- The fix should not introduce new dependencies; only restructure the internal storage and processing of queued updates.

fix(symfony): publish mercure updates for all resources of an entity (#7774)


The `IriConverter::getIriFromResource` method currently only handles `RuntimeException` when identifier extraction fails. However, `IdentifiersExtractorInterface::getIdentifiersFromItem` can also throw `\InvalidArgumentException` (or `ApiPlatform\Exception\InvalidArgumentException`) for certain invalid item states. This causes the converter to bypass the fallback that checks for context URI variables, and the raw extraction exception leaks out instead of producing a descriptive error.

Make the converter handle any identifier-extraction failure uniformly: when `getIdentifiersFromItem` throws either `InvalidArgumentException` or `RuntimeException`, the converter must still attempt the existing fallback to context URI variables. If no context variables are available, it should throw an `InvalidArgumentException` with the usual message indicating the resource class (e.g., “Unable to generate an IRI for the item of type …”), preserving the original exception as the previous cause.

fix(symfony): catch identifiers exceptions in iri converter


## Backlog ticket: Route overlap when resource name is a prefix of another resource’s path

### Problem

In the Laravel integration, registering two API resources where one resource’s path is a strict prefix of another (e.g., `/api/staff` and `/api/staff_position_histories`) can cause route mismatches. A request intended for the longer path may be incorrectly dispatched to the shorter route, because the optional `_format` parameter in the generated route pattern captures trailing segments that look like extended path components rather than a content-format suffix.

Concretely, a GET to `/api/staff_position_histories?page=1` may resolve against the `/api/staff{_format?}` route, treating `_position_histories` as the value for `_format`. This leads to a 404, a wrong resource being served, or other unexpected routing behavior.

### Expected behavior

- Routes should only consume a `_format` value when it matches a true format extension (e.g., `.json`, `.jsonld`, `.html` — a literal dot followed by one or more letters).
- Path segments that do not begin with a dot must not be captured as a format parameter.
- Requests to `/api/staff_position_histories` must reach the `StaffPositionHistory` resource, while `/api/staff.json` or `/api/staff.jsonld` should still be routed to the `Staff` resource with the appropriate format.

### Constraints / safety

- The format constraint must apply to all API Platform–generated routes that use the optional `_format` parameter, not just specific resources.
- Must not break existing format negotiation for registered formats (e.g., `.jsonld`, `.jsonapi`, `.html`).
- Must not affect routes that do not use an optional format segment.

### Related

- Issue: #6779
- Area: Laravel route registration

fix(laravel): overlaping route format (#6782)


**Request:**
When constructing an `ApiPlatform\Metadata\Parameters` from an array of `Parameter` instances, duplicate entries that share both the same parameter class (e.g., `QueryParameter`) and the same key must be collapsed, with the last occurrence taking precedence. Entries of different parameter classes that happen to have the same key (for example, a `QueryParameter` with key `foo` and a `HeaderParameter` with key `foo`) must be treated as independent and both retained.

The behavior must be consistent with the existing lookup methods (`get` and `has`) that accept an optional `$parameterClass` argument: when `get('foo', QueryParameter::class)` is called, it should return the unique `QueryParameter` for key `foo` that survived deduplication; a call without the class argument defaults to `QueryParameter::class` and should behave accordingly. The `count()` of the collection must accurately reflect the number of distinct (class + key) pairs after deduplication and before any further modifications.

fix(metadata): eliminate duplicate keys in the constructor of `Parameters` (#7492)


**Issue: SearchFilter with `partial`/case-insensitive drops multiple values when they differ only in letter case**

When the `SearchFilter` is applied to a property with `strategy: partial` and `case_insensitive: true`, and the query provides multiple values that are distinct in case but identical once lowercased (e.g. `?name[]=blue+car&name[]=Blue+Car`), the generated database query only includes one `LIKE` condition and one bound parameter instead of one per input value. This causes results that match the dropped value(s) to be missing from the response.

**Expected behaviour**
Each value in the array should produce its own `LIKE` clause and its own parameter placeholder, so that `blue car` and `Blue Car` both contribute to the `OR` condition and matching entities are returned regardless of how the user typed the case differences.

**Steps to reproduce**
1. Configure a `SearchFilter` on a string property, e.g., with `'name' => 'ipartial'` (case-insensitive partial search).
2. Make a request with `?name[]=foo+Bar&name[]=Foo+Bar`.
3. Observe the generated query (e.g., via logging or a debug toolbar).

**Actual result**
The query contains a single `LOWER(name) LIKE LOWER(CONCAT('%', :name_p1_0, '%'))` with only one parameter placeholder, effectively searching for only one of the values.

**Impact**
Incomplete search results; filters with near-identical casing differences silently discard some criteria. The same problem may also affect the MongoDB ODM path.

**Environment**
API Platform Core, Doctrine ORM filter layer (likely also applies to Doctrine ODM).

fix(doctrine): use abitrary index instead of value (#5079)


**Title:** Abstract Eloquent models cause fatal error in `RelationMetadataLoader`

**Description**

When an abstract Eloquent model class (extending `Illuminate\Database\Eloquent\Model`) is used with API Platform, the `RelationMetadataLoader` tries to instantiate it via `newInstanceWithoutConstructor()`, which triggers a PHP fatal error because abstract classes cannot be instantiated. This prevents using abstract base models in API Platform resources.

**Expected behavior**

The relation metadata loader should detect that a class is abstract and skip it gracefully. For an abstract Eloquent model, the loader should return `false` (indicating no relation metadata was loaded) instead of throwing an error. This keeps abstract model classes usable as base types without requiring instantiation.

**Acceptance criteria**

- No fatal error or uncaught exception when an abstract Eloquent model class is encountered during relation metadata loading.
- The loader returns `false` for abstract model classes.
- Behavior for concrete (non-abstract) Eloquent models remains unchanged.

**Steps to reproduce** (illustrative)

1. Define an abstract class that extends `Illuminate\Database\Eloquent\Model`.
2. Reference that abstract class in your API Platform resource configuration.
3. Run the application and observe a fatal error (`Class ... cannot be instantiated as abstract`) originating from `RelationMetadataLoader`.

fix(laravel): skip relation metadata for abstract Eloquent models (#7933)


In the GraphQL type converter (`TypeConverter`), relations that are represented as arrays or iterables (e.g., when the property type is `array` of a resource class, without Doctrine) are currently always mapped to the `Iterable` scalar type. They should be treated like object-type relations: if the collection’s element type is itself an API Platform resource, the converter should return the corresponding GraphQL resource object type instead of `Iterable`.

Better handling of relations without Doctrine. (#3591)


Remove the `messenger=persist` attribute handling and replace it with a resumable chain mechanism.

## Context
Currently, to combine the Symfony Messenger data persister with the Doctrine persister, resources are configured with `messenger="persist"` (or an array like `messenger={"persist", "input"}`). The messenger persister uses a `messenger_dispatched` context flag and a `handOver` method to call Doctrine before the message is sent. This is being replaced.

## Desired behavior
- Introduce a `ResumableDataPersisterInterface` with a method `resumable(): bool`. When a data persister implements this interface and returns `true`, the chain data persister (`ChainDataPersister`) should not stop after its `persist`/`remove`; it should continue iterating and invoke any later persister that also supports the data.
- The debug wrapper (`TraceableChainDataPersister`) must correctly track which persisters were called and their support responses now that the chain can invoke multiple persisters per operation.
- The Symfony Messenger data persister must no longer require the chain persister as a dependency (its constructor should accept only the resource metadata factory and the message bus). It must not check for or use a `messenger_dispatched` context flag. Its `supports` should return `true` for any resource whose `messenger` attribute is non-false (boolean `true`, `"input"`, etc.).
- The messenger persister should implement `ResumableDataPersisterInterface` and always return `true` from `resumable()`, so the chain naturally continues to Doctrine after the message is dispatched.
- The Messenger data transformer should only support transformation when the resource’s `messenger` attribute is exactly the string `"input"` (not when it is an array containing `"input"` or `"persist"`). That code path should compare against `'input'` directly, without using `in_array`.
- Remove all special handling of `messenger="persist"`, the `handOver` logic, and the `messenger_dispatched` context flag from the messenger data persister.
- Any test fixtures or service configurations that existed solely to support the removed `messenger=persist` feature should be deleted, but leave the rest of the messenger integration intact.

Revert messenger=persist and introduce a ResumableDataPersisterInterface (#3912)


**Allow controller arguments with a name different from `$data` for the deserialized payload**

Currently, when API Platform deserializes the request body, the resulting object is injected into the controller action only if the action parameter is literally named `$data`. Any other name (e.g. `$payload`, `$input`) receives `null` or is not resolved.

We want to support any parameter name, as long as the parameter’s type-hint matches the expected input class for the current request, so that the deserialized data is automatically injected into the controller.

**Requirements:**
- Implement a Symfony `ArgumentValueResolverInterface` that resolves a controller argument if:
  - Its type-hint matches the resource class *or* the `input` class configured for the current operation.
  - The request is eligible for deserialization. The resolver must apply exactly the same eligibility checks that the existing `DeserializeListener` uses before it deserializes: the request must not be a safe method or `DELETE`, `_api_receive` must be true, and deserialization must not be disabled via the operation’s `deserialize` attribute.
  - The deserialized object is already available in the request attributes under the `data` key.
- The resolver should yield the value from `request.attributes.data` when it matches.
- The resolver must not resolve variadic arguments or arguments without a type declaration.
- The existing behavior where a parameter named `$data` still receives the payload must continue to work (i.e. the resolver should coexist with, not break, the current mechanism).
- Register the new resolver as a service with the `controller.argument_value_resolver` tag so that it is automatically discovered by Symfony’s argument resolution chain.
- The deserialization-eligibility logic that is currently inline in `DeserializeListener` must be reused (not duplicated) between the listener and the new resolver to keep them in sync.

**Non-goal:** Changing how the deserialized data is stored in the request attributes; the `data` key remains as-is.

feat: add payload controller value resolver (#3263)


**Title**: Laravel Eloquent PropertyAccessor mishandles null attribute values

**Description**
When using API Platform’s Laravel integration with Eloquent models, attributes that are `null` are not correctly read or written. The custom `PropertyAccessor` for Eloquent delegates to Symfony’s generic property accessor for most operations, bypassing Eloquent’s `__get`/`__set` magic and attribute-casting layer. This leads to errors or incorrect values (e.g. empty strings instead of `null`) when:

- Creating or updating a resource without providing a nullable field
- Reading a model where a nullable column contains `null`

The workbench Book resource currently has a non‑nullable `publication_date`, so the problem is hidden in the existing tests; however, any model with nullable columns is affected.

**Expected behavior**
For any instance of `Illuminate\Database\Eloquent\Model`, the `PropertyAccessor` should:

- Read and write attributes directly via `$model->{$propertyPath}` so that Eloquent’s attribute handling (including null values, accessors, and mutators) is used.
- Report `isWritable` and `isReadable` as `true`, since Eloquent models support dynamic attribute access for all columns.

**Steps to reproduce**
1. Define an API resource for a Book model that has a nullable column (e.g., a `publication_date` that allows `NULL`).
2. Send a POST request to create a book without including the `publicationDate` attribute (or explicitly setting it to `null`).
3. Observe that the request fails or that the stored value is not `null`. Reading the resource afterward may also omit the field or return an unexpected value.

**Additional context**
To expose the bug and ensure it stays fixed, the `publication_date` column in the workbench migration and factory should be made nullable. This will let the existing tests exercise the null‑value path for both JSON‑LD and JSON‑API serialization.

fix(laravel): Eloquent PropertyAccessor (#6536)


When an API Platform resource operation is configured with an explicit `requestBody` via the `openapi` attribute (setting `requestBody` in the `OpenApiOperation` metadata), the generated OpenAPI specification ignores that custom request body for POST, PUT, and PATCH endpoints. Instead, the OpenAPI factory always emits a schema-derived request body, overwriting the user-provided one. This makes it impossible to fully customize the request body description, content types, required flag, or schema for write operations through the documented `openapi` operation decoration.

Steps to reproduce:
1. Define a POST, PUT, or PATCH operation on a resource, and attach an `openapi` metadata object that includes `requestBody` with a custom description, content, and required flag.
2. Generate the OpenAPI spec (e.g., via the `/docs.json` endpoint or an `openapi:export` command).
3. Inspect the operation’s request body in the generated spec.

Expected: The generated request body matches exactly what was defined in the `openapi` metadata.
Actual: The custom request body is replaced by the standard auto-generated request body derived from the resource schema.

The impact is that users cannot override the request body definition for write operations using the `openapi` integration point; any custom schema, examples, or descriptions are silently discarded. This breaks the promise that the `openapi` operation attribute can be used to fine-tune the generated spec.

Please update the OpenAPI factory logic so that when an operation already carries a request body (set via `openapi` metadata), the auto-generation step for POST/PUT/PATCH is skipped, preserving the user-provided request body exactly as specified.

fix(openapi): fix openapi requestbody decoration (#5377)


**Title:** Missing `is_collection` in Elasticsearch filter descriptions

**Description:**
When using the Elasticsearch search filters (`MatchFilter` and `TermFilter`), the filter parameter descriptions returned by `getDescription()` do not include an `is_collection` key. This key is already present in the equivalent Doctrine ORM `SearchFilterTrait` and tells API documentation consumers (Hydra, OpenAPI) whether a parameter expects an array of values. Its absence means generated documentation for Elasticsearch-backed APIs cannot distinguish between scalar filter parameters (e.g., `name`) and array filter parameters (e.g., `name[]`), leading to a confusing developer experience for API consumers.

**Steps to Reproduce:**
1. Configure a `MatchFilter` or `TermFilter` for an Elasticsearch resource.
2. Call `$filter->getDescription()` on that filter.
3. Inspect the returned description arrays – they contain `property`, `type`, and `required` but not `is_collection`.

**Expected Behavior:**
Each filter parameter description should include an `is_collection` field, with the value `true` when the parameter name ends with `[]` and `false` otherwise. This would mirror the behavior of the Doctrine ORM search filters and allow documentation tools to correctly represent array-valued query parameters.

**Additional Context:**
The change likely belongs in the `getDescription` method of `AbstractSearchFilter`, which is shared by both concrete Elasticsearch filter implementations. The logic to determine the value can follow the existing convention used in the Doctrine filters: if the filter parameter name ends with `[]`, `is_collection` is `true`; otherwise `false`.

fix(elasticsearch): add is_collection to documentation (#5497)


Fix metadata inheritance for `is`/`can` accessors and ensure GraphQL operations inherit from resource metadata.

The `WithResourceTrait::copyFrom` only copies properties accessed via `get*` accessors. Many operation properties are exposed through `is*` (e.g., `collection`) or `can*` (e.g., `read`, `write`) methods, so those are never inherited when building an operation from an `ApiResource` via `withResource` or `withOperation`. Additionally, `copyFrom` uses a “skip if already non‑null or non‑empty array” rule, which prevents inheritance even for `get*` properties when the target operation has non‑null defaults. GraphQL operations initialize boolean flags to `true`/`false` and array contexts to empty arrays, so they always appear “set” and never inherit.

Required changes:
1. Update `copyFrom` to handle `get`, `is`, and `can` prefixes. The method should copy a value only when the target object’s property is `null` (meaning unset) and the source’s value is not `null`. No special treatment for empty arrays.
2. Change the GraphQL `Operation` class so that all properties eligible for inheritance have `null` as their constructor default and their getters return a nullable type (e.g., `?bool`, `?array`, `?string`, `?int`). Example properties: `$read`, `$deserialize`, `$validate`, `$write`, `$serialize`, `$order`, `$normalizationContext`, `$denormalizationContext`, `$filters`, `$validationContext`, `$priority`, `$name`.
3. In `LegacyResourceMetadataResourceMetadataCollectionFactory::setAttributeValue`, skip calling the `with*` method when the value to set is `null`, to avoid overwriting operation defaults with null from legacy metadata.

fix(metadata): call is/can/get methods within copyFrom (#4463)


**Title:** GraphQL query for non-existent single item throws error instead of returning `null`

**Description:**
When using the GraphQL endpoint to fetch a single resource by its identifier (e.g. `book(id: "/books/1")`), a missing resource currently results in an unhandled `ItemNotFoundException` (or a `LogicException` downstream). The expected behavior for GraphQL `Query` operations is to return `null`—the field should resolve to `null` without triggering an error, consistent with how nullable fields work in GraphQL.

The issue appears to stem from the `ReadProvider` used for GraphQL state provision. Internally, it resolves the item through the IRI converter. When the resource is not found, the IRI converter now throws `ApiPlatform\Metadata\Exception\ItemNotFoundException`, but the provider’s exception handling still references the old namespace (`ApiPlatform\Exception\ItemNotFoundException`) and therefore doesn’t catch the exception. Even after the exception is caught, the provider does not properly handle a `null` item for `Query` operations—it later throws a `LogicException` because it expects the item to always be an object.

**Expected behavior:**
- For a GraphQL `Query` (single item) operation, if the IRI converter cannot find the resource, the `ReadProvider` should return `null`.
- No `ItemNotFoundException` or `LogicException` should be surfaced to the client.

**Steps to reproduce:**
1. Send a GraphQL query for a specific resource using its IRI, e.g.:
   ```graphql
   { book(id: "/api/books/999") { title } }
   ```
2. Observe that instead of `{"data": {"book": null}}`, the response contains an error referencing an `ItemNotFoundException` or a `LogicException`.

**Additional context:**
- This only affects GraphQL `Query` operations, not collections.
- The REST API correctly returns a `404` for missing items, but GraphQL semantics require `null` for non-existent resources.
- The exception class for “item not found” has moved to `ApiPlatform\Metadata\Exception\ItemNotFoundException`. The provider needs to reference the correct class and handle the result gracefully.

fix: return null instead of exception for GraphQL Query operation (#6118)


**Title:** API resources using `#[ApiResource]` attribute are not autoconfigured – `resource_class_directories` still required

**Problem**
Classes annotated with the `#[ApiResource]` attribute are not automatically recognized as API Platform resources. To make them work, I still must explicitly list them via the `resource_class_directories` configuration key. If I omit that config, the resource endpoints are either missing (404) or the resource metadata is not loaded.

This is inconsistent with how Symfony’s autoconfiguration works for other tagged services and attributes. For example, API Platform already auto-discovers filters via the `#[ApiFilter]` attribute; it should do the same for resources.

**Expected Behavior**
When a Symfony application has autoconfiguration enabled (the default), any class marked with the `#[ApiResource]` attribute should be automatically registered as an API resource, without needing any `resource_class_directories` entry.

Additionally:
- The `resource_class_directories` configuration option should be deprecated because manual directory scanning is no longer necessary.
- The discovery must still work correctly even if those resource classes are not instantiated as real services (i.e., they are excluded from the container). The autoconfiguration should add the necessary tag(s) so a compiler pass can collect the class names later.
- The mechanism should integrate with the existing `api_platform.class_name_resources` parameter so that metadata is built correctly.

**Additional context**
API Platform 3.x / 4.x relies on metadata from `#[ApiResource]` attributes. The current workaround is to configure `resource_class_directories` or to manually scan directories – which is a poor developer experience in modern Symfony applications that use autoconfiguration pervasively. Making this work automatically will reduce boilerplate and bring the resource discovery in line with how other API Platform components (filters, data providers, etc.) are already autoconfigured.

feat(symfony): Autoconfigure classes using `#[ApiResource]` attribute (#6943)


**Title:** TLS 1.3 client Certificate message missing `certificate_request_context`

**Background**
Per RFC 8446 §4.4.2, the TLS 1.3 Certificate message begins with a `certificate_request_context` field. For the initial handshake this is a zero-length context; a non-zero context is reserved for post-handshake client authentication (§4.3.2), which s2n does not yet implement.

**Current behavior**
The server Certificate message already respects this: it writes the context length byte on send and validates it on receive. The **client** Certificate message does **not** include the `certificate_request_context` at all, causing protocol non‑compliance in TLS 1.3 handshakes that use client certificates.

**Requested change**
Update the client Certificate send and receive paths to include the `certificate_request_context` field when the negotiated version is TLS 1.3.

Requirements:
- **Send side**: before serializing the certificate chain, write a single `uint8_t` with value `0` representing a zero‑length context.
- **Receive side**: read the context length byte and verify it is `0`. If any other value is encountered, fail the handshake immediately with a protocol error (`S2N_ERR_BAD_MESSAGE`).
- **Backward compatibility**: the behavior for TLS 1.2 and below must remain unchanged (no context field).

Since the server side already implements the exact same logic (write a zero‑length context, read back and enforce zero), the client implementation should mirror that pattern. The only difference is the target message direction.

**References**
- RFC 8446 §4.4.2 (Certificate) and §4.3.2 (post‑handshake authentication)
- Existing server certificate handler for an example of the expected format.

Add certificate_request_context to TLS 1.3 client cert (#1853)


**Title:** `S2N_ERR_TOO_MANY_SIGNATURE_SCHEMES` triggers too early and the peer scheme list is not always correct for selection

**Observed behavior**
When a TLS peer (client or server) sends a `signature_algorithms` extension or a `CertificateRequest` that contains more than 64 signature schemes, the handshake is aborted with `S2N_ERR_TOO_MANY_SIGNATURE_SCHEMES`. Real-world TLS 1.3 clients often advertise more than 64 schemes (e.g., 80–100), causing interoperability failures. Additionally, the error message currently claims the maximum is 32, but the actual limit enforced is 64, which is misleading.

Separately, the list of schemes received from the peer is stored in two different handshake fields depending on the connection mode (`client_sig_hash_algs` on the server side, `server_sig_hash_algs` on the client side). This means that the signature selection logic (which inspects what the *peer* supports) must always know which list to consult, and it is currently only using one of them. In scenarios where both a client authentication list and a server authentication list are needed (TLS 1.3 mutual authentication), the selection may therefore look at the wrong list.

**Expected behavior**

1. **Higher limit** – The maximum number of offered signature schemes should be increased so that s2n‑tls can complete handshakes with peers that send more than 64 (and certainly more than 32) schemes. A limit of 128 entries would cover real-world usage while still being safe.
2. **Consistent limit across paths** – The same bound must be applied to every TLS message that carries a scheme list: the client’s `signature_algorithms` extension, the server’s `CertificateRequest` in TLS 1.2, and the TLS 1.3 `CertificateRequest`.
3. **Unified peer list storage** – Regardless of whether s2n‑tls is acting as a client or server, the list of schemes offered by the peer must be kept in a single, connection‑wide location. The signature selection logic (`s2n_signature_algorithm_select` and its helpers) should be able to simply consult “the peer’s schemes” without branching on the local role. This ensures that both the server‑side certificate selection and the client‑side certificate selection always use the correct peer capabilities.

**Implementation constraints / safety**

- The bound is defined by `TLS_SIGNATURE_SCHEME_LIST_MAX_LEN`; raising it to 128 must be accompanied by a check in `s2n_recv_supported_sig_scheme_list` (and anywhere else that reads such a list) so that lists longer than the new maximum still produce `S2N_ERR_TOO_MANY_SIGNATURE_SCHEMES`.
- The internal storage (`s2n_sig_scheme_list`) already uses a fixed‑size `iana_list` array whose length is set by that macro; changing the constant will automatically adjust the capacity.
- The existing callers that receive peer schemes (the client and server signature‑algorithm extension handlers, plus the `CertificateRequest` receiver) must all write into the unified storage.
- The validation function `s2n_signature_algorithms_validate_supported_by_peer` must be updated to look at the unified peer list rather than picking one of the two old fields.
- The overall memory footprint increase is negligible (a few hundred bytes per connection).

**Impact**
Without this fix, s2n‑tls cannot establish TLS connections to a growing number of legitimate clients and servers that exceed the current 64‑scheme limit, and mutual‑authentication flows may select an unsupported signature scheme because the wrong peer list was consulted.

fix: Increase received signature scheme limit (#4544)


```
Fix memory leak during map resize and make map completion explicit

The s2n_map currently leaks memory whenever its internal table grows
(embiggen).  The resize copies entries into a new table, but the old
key and value blobs that were allocated via s2n_dup are never freed.
Make sure those allocated blobs are released when the old table is
discarded.

Additionally, the map has been implicitly marking itself as “complete”
(read-only) on the first call to s2n_map_lookup.  Replace that implicit
behavior with a new public function s2n_map_complete().  After
completion, all subsequent calls to s2n_map_add must fail (as they
already do when the map is marked immutable).  s2n_map_lookup must
return an error if the map has not yet been completed, instead of
silently finishing the map on first lookup.
```

Fix memory leak and make map completion explicit


**Title:** Supported versions extension mishandles unrecognized version entries

**Description:**
When a client sends the `supported_versions` extension containing version codes that s2n does not support (such as GREASE values, future TLS versions, or arbitrary 2-byte values), the server currently treats every entry in the list as a valid client version. This leads to two problems:

1. The connection’s `client_protocol_version` is set to the numerically highest value in the list, even if that value does not correspond to a real TLS version.
2. Negotiation may select a version that is not actually supported by either side, resulting in interop failures, unexpected fallback to a lower version, or connection aborts later in the handshake.

For example, a legitimate client might send `[TLS 1.3, GREASE values, TLS 1.2]`. The server should recognize TLS 1.3 and TLS 1.2 as valid, skip the GREASE entries, and successfully negotiate TLS 1.3. Instead, the GREASE value often appears numerically larger than any real version code, causing the server to incorrectly believe the client requested an unsupported version and negotiate the wrong protocol version (or fail).

**Expected behavior:**
Entires in the `supported_versions` extension that are not recognised TLS versions should be silently ignored during negotiation. Specifically, an entry should only be considered if the major version byte is 3 and the minor version byte is between 0 and 4 inclusive (SSL 3.0 through TLS 1.3). GREASE values and any future/unknown versions that fall outside that range must be skipped.

If after filtering no recognised version remains, the server should abort the handshake with `S2N_ERR_BAD_MESSAGE`, since there is no mutually supported version.

**Impact:**
This directly affects interop with clients that use GREASE or that advertise larger version numbers than currently known. Incorrect version negotiation can also trigger security-sensitive paths (e.g., selecting a weaker cipher suite) or cause connection failures that are hard to diagnose.

**Note:**
Care should be taken that the server still records the original `client_protocol_version` based on the highest *recognised* version in the list, not the raw list order. The negotiated `actual_protocol_version` must still reflect the highest version supported by both client and server among the filtered entries.

In the supported versions extension, ignore unknown versions. (#1626)


**Feature request: Automatically derive TLS extension requirements from cipher suite key exchange algorithms**

**Context**
s2n allows applications to select cipher preferences (via `s2n_config_set_cipher_preferences`) that control which cipher suites are offered during the TLS handshake. Certain cipher suites require specific TLS extensions to be sent in the ClientHello:
- Cipher suites using ECDHE key exchange require the Supported Elliptic Curves and Supported Point Formats extensions (RFC 4492).
- Cipher suites using post-quantum/hybrid key exchange (e.g., s2n_hybrid_ecdhe_kem) require the PQ KEM Parameters extension.

Currently, the library relies on a manually maintained flag (`extension_flag`) in the cipher preferences struct to decide whether to include the ECC extensions. There is no equivalent flag for PQ KEM extensions; instead, the decision is made by traversing the cipher suite list each time a ClientHello is built. This hybrid approach is inconsistent and error‑prone:
- A newly added cipher preference can accidentally omit the ECC flag even though its suites use ECDHE, causing the server to reject the handshake.
- The PQ KEM extension computation is done repeatedly instead of being pre‑computed once.

**Desired behavior**
1. **Automatic derivation**: When the library initializes, it should scan every registered cipher preference and automatically determine, based solely on the key exchange algorithms of its cipher suites, whether it requires the ECC extensions and the PQ KEM extensions. The result should be stored so that queries during the handshake are fast.
2. **Consistent interface**: There should be separate, clear predicates for “does this cipher preference require the ECC extensions?” and “does this cipher preference require the PQ KEM extensions?”. Both should return a boolean decision derived from the same pre‑computed data.
3. **ClientHello behavior unchanged**: The logic in `s2n_client_extensions_send` should use the new predicates. The ECC extensions must only be included when ECDHE or hybrid key exchange is present in the configured cipher preference. The PQ KEM extension must only be included when hybrid key exchange is present. The total extension size calculation and the actual writing of extension bytes should respect these decisions.
4. **Removal of manual flag**: The `extension_flag` field and its constant `S2N_ECC_EXTENSION_ENABLED` should be eliminated. No cipher preference definition should need to set any manual flag.
5. **Handling of unknown preferences**: If a caller passes a cipher preference pointer that was not registered during library initialization (e.g., a custom `s2n_cipher_preferences` struct constructed by external code), the predicates must fail safely with an error rather than silently returning a misleading result. This prevents undefined behavior when the library cannot infer the extension requirements.
6. **No runtime overhead**: The extension‑requirement lookup for known preferences must be O(1) after initialization. The iteration over cipher suites should happen exactly once at library init.

**Acceptance criteria**
- Using `s2n_config_set_cipher_preferences` with any built‑in profile that includes an ECDHE suite (e.g., “test_all”, “20170210”, “KMS‑TLS‑1‑0‑2018‑10”) results in the ClientHello including the ECC extensions.
- Using a profile that includes PQ suites (e.g., “test_all”, “KMS‑PQ‑TLS‑1‑0‑2019‑06”) results in the ClientHello including the PQ KEM extension.
- Using a profile without ECDHE (e.g., “20141001”) does **not** include ECC extensions.
- Using a profile without PQ suites (e.g., “KMS‑TLS‑1‑0‑2018‑10”) does **not** include the PQ KEM extension.
- Passing a NULL preference pointer or an unregistered custom preference to the lookup functions returns an error.
- The `extension_flag` field is completely removed from the `s2n_cipher_preferences` struct and from all preference definitions.
- The library initialization sequence (`s2n_init`) performs the automatic computation immediately after cipher suites are initialized.


When a TLS client does not provide a supported_groups extension (i.e., it does not specify any preferred curves), the server must select a default curve for ECDHE key exchange. According to RFC 8446 Section 9.1, a compliant implementation MUST support secp256r1 (NIST P-256) and SHOULD use it for key exchange. Currently, s2n falls back to the first curve listed in the server’s configured ECC preferences, which may not be secp256r1. This can cause handshake failures or interoperability problems with clients that rely on the spec-mandated default.

**Acceptance Criteria:**

1. **Fallback curve** – When the server receives a ClientHello that omits the `supported_groups` extension (or otherwise provides no curve preference), the server must select secp256r1 (NIST P-256) as the negotiated curve for ECDHE key exchange, regardless of the order of curves in the configured security policy’s ECC preferences.

2. **Security policy validation** – At startup, s2n should verify that every non‑null security policy includes `secp256r1` in its ECC curve list. Any policy missing this curve must be rejected as invalid, because its absence would make the mandatory fallback impossible and violate the TLS specification requirement.

3. **Backward compatibility** – The change must not alter the curve selection when the client *does* specify a supported curve (i.e., the server should still respect the client’s preference as before). Only the fallback for a missing extension changes.

4. **Edge cases** – The special `security_policy_null` policy (used internally for minimal configurations) is exempt from the `secp256r1` inclusion check, as it does not participate in normal handshakes. All other built-in and custom policies must satisfy the requirement.

Use NIST P-256 for key generation when client do not specify curve (#2265)


**Bug: `s2n_tls13_compute_pq_hybrid_shared_secret` reads the PQ shared secret from the wrong location for client connections**

The function `s2n_tls13_compute_pq_hybrid_shared_secret` is responsible for concatenating the ECDHE and post-quantum shared secrets into a single hybrid secret during a TLS 1.3 handshake.
It currently obtains the PQ portion by reading `conn->secure.server_kem_group_params.kem_params.shared_secret`, **regardless of whether the connection is a client or a server**.

In the actual handshake, however, the two sides store the PQ shared secret in different fields:

- **Server**: generates the PQ shared secret and stores it in
  `conn->secure.server_kem_group_params.kem_params.shared_secret`.
- **Client**: decapsulates the server’s ciphertext and stores the derived shared secret in
  `conn->secure.chosen_client_kem_group_params->kem_params.shared_secret`.

Because the function always reads from `server_kem_group_params`, it silently reads an uninitialized (or stale) blob when called on a client connection, leading to handshake failures or derivation of incorrect traffic secrets on the client side.

**Expected behavior**
`s2n_tls13_compute_pq_hybrid_shared_secret` must inspect `conn->mode` and retrieve the PQ shared secret from the field appropriate for that mode:

- `S2N_SERVER` → use `server_kem_group_params.kem_params.shared_secret`
- `S2N_CLIENT`  → use `chosen_client_kem_group_params->kem_params.shared_secret`

The rest of the function (concatenation, null checks, length verification) should remain unchanged.

**Impact / note on testing**
The existing unit‑test helper `set_up_conns` also incorrectly populates the client’s PQ shared secret into `server_kem_group_params`, which hides the bug in the test suite.
When fixing the production code, the test helper should be updated to place the client’s shared secret in `chosen_client_kem_group_params` instead, so that the test accurately reflects the real handshake and exercises the mode‑dependent retrieval path.

PQ shared secret will be stored based on Client/Server mode (#2165)


**Title:** s2n should disable TLS alert processing and sending when QUIC mode is active

**Problem description:**

When a connection is used for QUIC (after calling `s2n_connection_enable_quic`), s2n is expected to delegate all alert handling to the QUIC layer. In practice, several TLS alert paths remain active:

- On the write side, close-notify and other handshake-failure alerts are still enqueued into the output buffer when the connection is torn down. This produces spurious TLS alert data that the QUIC layer does not expect.
- On the read side, `s2n_process_alert_fragment` still attempts to parse alert fragments from the input buffer. Receiving a TLS alert in QUIC mode is a protocol violation and should not be processed silently.

These leftover behaviors lead to unexpected data in the output stream and can cause interoperability problems with QUIC implementations that use s2n’s TLS stack.

**Expected behavior:**

- When QUIC mode is enabled on a connection, all alert-enqueuing functions become safe no-ops: they must still honor existing short-circuit conditions (e.g., an alert already queued, a close_notify already marked as sent) but must not write any new alert bytes into the output stuffer.
- `s2n_process_alert_fragment` must immediately reject any alert fragment when QUIC mode is active, returning an error (e.g., `S2N_ERR_BAD_MESSAGE`) to indicate that TLS alerts are not expected in this context.
- Alert functions that receive a `NULL` connection argument should return an error gracefully instead of dereferencing a null pointer, because the QUIC-mode check requires inspecting the connection.

The change should cover the main public alert APIs:

- `s2n_process_alert_fragment`
- `s2n_queue_writer_close_alert_warning`
- `s2n_queue_reader_*_alert` family (e.g., `s2n_queue_reader_handshake_failure_alert`) so that both reader and writer alert paths are gated.

No existing behavior should change when QUIC mode is not enabled.

Disable alerts when quic enabled (#2303)


```
Title: Incorrect handler dispatched for ChangeCipherSpec records

When processing an incoming ChangeCipherSpec (CCS) record during the TLS handshake, the code currently calls the handler tied to the current active state. In TLS 1.3, a CCS record may appear at any point in the handshake and must always be handled by the dedicated CCS handler (not the handler for whatever message was expected next). The state machine should not advance when the CCS is unexpected, but the handler invoked must still be the CCS‑specific one. This part was overlooked in a previous change that correctly stopped state advancement for unexpected CCS in TLS 1.3.

Additionally, for TLS 1.2, receiving a CCS record when the handshake is not expecting one is a protocol error. Such unexpected CCS records should cause an immediate handshake failure with the error code `S2N_ERR_BAD_MESSAGE`, rather than silently invoking an inappropriate state handler.

Please update the handshake record reading path to:
- Always dispatch to the appropriate CCS handler when a CCS record is processed (the handler for `SERVER_CHANGE_CIPHER_SPEC` when acting as client, or `CLIENT_CHANGE_CIPHER_SPEC` when acting as server).
- Reject unexpected CCS records in TLS 1.2 with `S2N_ERR_BAD_MESSAGE`.
- Continue to advance the state machine only when the CCS record was expected (expected record type is `TLS_CHANGE_CIPHER_SPEC`).
```

Fix processing of unexpected CCS messages


**Title**: Dangling pointer in `domain_name_to_cert_map` when `s2n_config_add_cert_chain_and_key` is called twice

**Description**
When using the deprecated C API `s2n_config_add_cert_chain_and_key` to add a library-owned default certificate, a second call with a certificate of the same key type fails with `S2N_ERR_MULTIPLE_DEFAULT_CERTIFICATES_PER_AUTH_TYPE`. However, before that error is raised the new certificate chain has already been inserted into the config’s domain name map (`domain_name_to_cert_map`). On the error path the certificate chain is freed by automatic cleanup, leaving the map with a dangling pointer. Subsequent SNI lookups may then dereference freed memory, leading to a use-after-free.

The same issue affects `s2n_config_add_cert_chain`.

**Expected behavior**
- No partial state changes (especially map insertions) should survive a failed certificate addition. If the addition is going to fail because of a duplicate default certificate, the domain name map must not be updated.
- More generally, the internal helper that both deprecated and store-based APIs rely on should perform all fallible checks *before* inserting into the domain name map, so that cleanup never leaves the map holding a reference to an about-to-be-freed certificate.
- The deprecated `s2n_config_add_cert_chain_and_key` and `s2n_config_add_cert_chain` functions effectively cannot support multiple calls for library-owned certificates because the ownership model only tracks one cert per type. It would be safer for them to reject a second call entirely with an ownership error rather than relying on a later check that arrives after already mutating the domain name map.

**Impact**
Use-after-free during SNI processing (possible crash or exploitable memory corruption).

fix: perform fallable checks before interting into domain name map (#5813)


**Title:** Send TLS 1.3 NewSessionTicket messages as post-handshake records

**Background**
After a TLS 1.3 handshake completes, the server may send one or more NewSessionTicket (NST) messages to the client so that the client can resume the session later. The server already has a function that constructs the NST handshake message content, but it currently writes the message directly into `conn->handshake.io` during the handshake phase. After the handshake finishes, `conn->handshake.io` is freed and any data written there is lost; therefore, NST messages never actually reach the client.

**Desired behavior**
1. Immediately after the TLS 1.3 handshake completes on the server side, if `conn->tickets_to_send` is greater than zero, the server must send that many NST messages as separate TLS records (using the record layer). Each ticket should be constructed using the existing ticket-key derivation logic, but the final output must go through `s2n_record_write` and be flushed to the I/O layer.
2. The sending must happen inside the post-handshake send hook that is already invoked from `s2n_negotiate` when the handshake moves to the application-data phase. Currently that hook (`s2n_post_handshake_send`) only handles KeyUpdate; it must also handle NST sending, honouring the remaining ticket count.
3. After all tickets have been sent, no more tickets should be emitted on subsequent calls to `s2n_negotiate` or `s2n_post_handshake_send`.

**Non-functional requirements / edge cases**
- Only send on the server side; on the client or on non-TLS1.3 versions the function should be a no-op.
- Honour `conn->tickets_to_send` and `conn->tickets_sent`. Send exactly `tickets_to_send - tickets_sent` tickets, updating `tickets_sent` as each is sent.
- Overflow safety: the increment of `tickets_sent` must be protected against integer overflow (the existing overflow check logic should be preserved, but applied inside the send loop).
- Blocking I/O: the send path must accept and propagate a `s2n_blocked_status` pointer so that the caller can react to WOULD_BLOCK conditions and resume later without losing state.
- The KeyUpdate send path (`s2n_key_update_send`) also directly calls flush, but it currently does not accept a `blocked` parameter. That signature should be updated so that both post-handshake message types are flushed consistently, and `s2n_post_handshake_send` itself no longer hard-codes a flush/rewrite on `conn->out`.
- Refactoring: separate the pure NST message construction from the record-write/flush loop. Provide a function that writes the handshake message bytes into a caller-supplied stuffer (e.g., a stack-allocated buffer), so the record layer can then wrap it. This keeps the message format logic unchanged and independently testable.

**Deliverable**
Implement sending of TLS 1.3 NewSessionTicket messages as described above, including the integration into the post-handshake flow and the necessary refactor of the message-construction function.


**Title:** SSLv3 handshakes fail when s2n-tls is built against OpenSSL 1.0.2 in FIPS mode

**Summary:**
Users building s2n-tls against OpenSSL 1.0.2 with FIPS enabled report that SSLv3 connections cannot be established. The handshake fails early during cryptographic setup, even though FIPS 140‑2 explicitly permits MD5 for the legacy SSLv3/TLS 1.0/1.1 pseudo‑random function. The same peer and configuration work correctly without FIPS or when using AWS‑LC.

**Expected behavior:**
SSLv3 connections should negotiate successfully in FIPS mode when the underlying libcrypto allows the legacy PRF. The s2n‑tls library must respect the FIPS exception for the SSLv3 key derivation material and must not artificially block SHA‑1‑based HMAC operations that have no dependency on MD5.

**Symptoms / failures to address:**

1. **HMAC availability incorrectly restricted**
   The availability check for `S2N_HMAC_SSLv3_SHA1` appears to be tied to the MD5 availability logic. SSLv3‑SHA1 uses SHA‑1, not MD5, so its availability should not be gated by whether MD5 is usable in FIPS mode. Only the MD5‑based SSLv3 HMAC (`S2N_HMAC_SSLv3_MD5`) needs to respect the FIPS/MD5 restriction.

2. **SSLv3 PRF loses its MD5‑for‑FIPS permission mid‑computation**
   In `s2n_sslv3_prf`, a one‑time call enables MD5 for FIPS before the output generation loop starts. However, each iteration of the loop resets the MD5 hash state, and the reset clears the FIPS allowance. Consequently, the first iteration succeeds but subsequent iterations (or even the first if reset is called before use) fail with an MD5‑disallowed error. The allowance must be re‑established after each hash state reset inside the loop.

**Acceptance criteria:**

- In FIPS mode using OpenSSL 1.0.2, a full SSLv3 handshake completes and application data can be exchanged.
- `S2N_HMAC_SSLv3_SHA1` is reported as available regardless of FIPS mode.
- The SSLv3 PRF can generate the complete master secret and key block in FIPS mode when the peer and configuration support SSLv3.
- No changes to non‑FIPS behaviour or to other protocol versions.
- The fix must not introduce any new security‑policy bypasses; MD5 usage remains restricted exactly to the legacy PRF as permitted by FIPS.

fix: SSLv3 handshake with openssl-1.0.2-fips fails (#4644)