Skip to content

Cover the whole Consul CE HTTP API - #70

Merged
lyrixx merged 22 commits into
mainfrom
more-services
Sep 29, 2026
Merged

lyrixx merged 22 commits into
mainfrom
more-services

Conversation

@lyrixx

@lyrixx lyrixx commented Sep 27, 2026

Copy link
Copy Markdown
Member

This PR adds all the Consul Community Edition (v2.0.x) HTTP API endpoints to the SDK. It can be reviewed commit by commit.

New services

ACL, Config, Connect (CA + intentions), Coordinate, DiscoveryChain, Event, Operator (raft, keyring, autopilot, usage), Peering (+ exported / imported services), PreparedQuery, Snapshot and Status.

Existing services

  • Agent, Catalog and Health now cover all their endpoints
  • All documented options are whitelisted (blocking queries, consistency modes, filter, ...) in Agent, Catalog, Health, KV and Session
  • Fix Agent::join() and Agent::forceLeave(): they sent GET, Consul only accepts PUT (405)

No BC break: nothing is removed nor renamed, only new optional arguments.

Client

  • The ACL token is read from the CONSUL_HTTP_TOKEN environment variable (like the Consul CLI); an explicit X-Consul-Token header wins
  • Multi-valued options are supported: ['tag' => ['v1', 'primary']] is sent as ?tag=v1&tag=primary (Symfony HttpClient would send ?tag[0]=v1&tag[1]=primary, which Consul does not understand)
  • The dc option is allowed on all endpoints, except the ones local to the agent, the datacenter listings, and the catalog (de)registration (where it goes in the payload). A test checks every method forwards it.

Out of scope

  • Enterprise-only endpoints and parameters (ns, partition, operator area / license / segment / audit / utilization, OIDC...)
  • Deprecated endpoints (intentions by ID, POST /v1/connect/intentions)
  • Streaming endpoints (/v1/agent/monitor, /v1/agent/metrics/stream)
  • /v1/internal/*

Tests & tooling

  • The CI Consul agent now runs with ACLs enabled (default_policy = allow, root management token)
  • Integration tests against a real Consul 2.0 agent for almost every method; endpoints that can't run on a single dev agent, or would break it (leave, force-leave, raft, keyring, peering establish, ACL bootstrap), are tested with MockHttpClient
  • PHPStan now runs against PHP 8.2 (the minimum supported version)
  • README: services table, badges removed

Add the missing /v1/agent endpoints, whitelist documented options, and fix join() and forceLeave() which used GET while Consul only accepts PUT.
Add connect(), nodeServices() and gatewayServices(), and whitelist the documented options.
Add connect() and ingress(), and whitelist the documented options.
Allow blocking queries (index, wait) on get() and check-and-set (cas) on delete().
Allow blocking queries and consistency modes on info(), node() and all().
Consul expects multi-valued parameters to be repeated (?tag=a&tag=b), but
Symfony HttpClient encodes arrays PHP-style (?tag[0]=a&tag[1]=b).
nicolas-grekas added a commit to symfony/symfony that referenced this pull request Sep 28, 2026
… `body` options (lyrixx)

This PR was merged into the 8.2 branch.

Discussion
----------

[HttpClient] Allow repeating fields in the `query` and `body` options

| Q             | A
| ------------- | ---
| Branch?       | 8.2
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Issues        | -
| License       | MIT

Many HTTP APIs expect multi-valued query parameters to be sent as repeated keys (`?name=a&name=b`), not PHP-style (`?name[0]=a&name[1]=b`). It is the default serialization of arrays in OpenAPI 3 (`style: form`, `explode: true`), and what Go's `net/url`, Python's `requests` and many others produce.

Today, the `query` option can't express this, and neither can array bodies: arrays always go through `http_build_query()`. The only way is to build the URL by hand, which defeats the purpose of the option (and of the default `query` of scoped clients).

This has been discussed twice already: #33488 and #53489 (`flatten_query_string`). Both were closed because adding an *option* to switch the encoding is not the right approach. In #53489, `@nicolas`-grekas suggested another way ([comment](#53489 (comment))):

> What we need to solve is the representation of arbitrary query strings as arrays that we can serialize to query strings. [...]
> ```php
> $query = [
> 	['foo', 'bar'],
> 	['foo', 'baz'],
> ];
> ```
> Because the root array passes array_is_list, we could decide to serialize it using something else than http_build_query.
> We'd need to define something natural that's safe from mistakes as much as possible. E.g I would forbid nested arrays in this representation.

This PR implements that idea, writing each pair as a single-entry array, the convention `FormDataPart` already uses for repeated form fields. No new option, no change in the contracts:

```php
$client->request('GET', 'http://127.0.0.1:8500/v1/connect/intentions/match', [
    'query' => [['by' => 'destination'], ['name' => 'web'], ['name' => 'api']],
]);
// GET /v1/connect/intentions/match?by=destination&name=web&name=api

$client->request('POST', $url, [
    'body' => [['tag' => 'a'], ['tag' => 'b'], ['file' => fopen('a.pdf', 'r')], ['file' => fopen('b.pdf', 'r')]],
]);
```

I need this for the [Consul PHP SDK](https://github.com/FriendsOfPHP/consul-php-sdk), which is built on top of HttpClient. The [Consul HTTP API](https://developer.hashicorp.com/consul/api-docs) uses repeated keys, e.g. `name` on [`/v1/connect/intentions/match`](https://developer.hashicorp.com/consul/api-docs/connect/intentions#list-matching-intentions) ("You can repeat this parameter for batching multiple matches"), or `node-meta` on the health endpoints. The SDK currently has to take multi-valued parameters out of the `query` option and append them to the URL by hand ([code](FriendsOfPHP/consul-php-sdk#70)).

How it works:
* When `query` or `body` is a list whose first item is an array, each item must be an array with a single `name => value` entry, encoded with the same escaping as today; string keys can't be mixed in the same array (`['by' => 'x', ['name' => 'web']]` keeps the associative meaning);
* Names can be repeated; brackets must be explicit (`['tag[]' => 'a']` gives `tag[]=a`);
* Values must be scalars, `\Stringable`, `\BackedEnum` or `null`, plus streams in bodies (to upload several files under the same name); anything else, including nested arrays, throws an `InvalidArgumentException`;
* A `null` value removes the parameter from the URL, like with the associative form, and is skipped in bodies;
* A name passed in the option replaces all the occurrences of that name in the URL, and default `query` options still don't override the request ones (it works with `ScopingHttpClient` too); in the URL, values that share a name are grouped where the name first appears (`[['a' => 1], ['b' => 2], ['a' => 3]]` gives `a=1&a=3&b=2`), while bodies keep the order of the list.

The associative form is unchanged. What changes is lists whose first item is an array: `[['a' => 'b']]` used to be encoded as `0[a]=b`, and shapes like `[[1, 2, 3]]` now throw. Since `parse_str()` and `$request->query->all()` can produce such lists (`?0[a]=b`), forwarded input should be filtered with an allow-list, not by removing keys.

Commits
-------

34e5866 [HttpClient] Allow repeating fields in the `query` and `body` options
@lyrixx
lyrixx merged commit e076fb4 into main Sep 29, 2026
7 checks passed
@lyrixx
lyrixx deleted the more-services branch September 29, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant