= API Design == HTTP Paths Where possible, each v2 API is given an HTTP path that reflects the resource type and/or name most relevant to its functionality. Resource types are typically plural nouns such as "aliases", "collections", and "shards". Resource names are (typically user-provided) identifiers such as "myAlias", "techproducts", and "shard1". For example, `/api/collections` is the HTTP path used for all APIs concerned with collections generally, but that don't involve any one specific collection (e.g. listing all collections). APIs that concern themselves with a specific collection use the HTTP path `/api/collections/someCollectionName`. Resource types and names are arranged in the HTTP path such that each path segment is more specific, or "narrower", than the segment that came before. This "narrowing" also extends to resources that have an "is part of" or "contains" relationship to one another. In these cases all relevant resources and their types are included in the path, with the "contained" or "child" resource following its "parent". For example, since replicas always belong to a shard, and shards always belong to a collection, most v2 APIs pertaining to a specific replica use the HTTP path: `/api/collections/specificCollectionName/shards/specificShardName/replicas/specificReplicaName`. Following these guidelines has given us the following (non-exhaustive) list of v2 API paths, provided here to give a good sense of the paths currently in use and the logic underlying them. * `/api/aliases` * `/api/aliases/specificAliasName` * `/api/aliases/specificAliasName/properties` * `/api/aliases/specificAliasName/properties/specificPropertyName` * `/api/backups/specificBackupName` * `/api/backups/specificBackupName/versions` * `/api/backups/specificBackupName/versions/specificVersion` * `/api/cluster/nodes/specificNodeName/roles` * `/api/cluster/nodes/specificNodeName/roles/specificRoleName` * `/api/cluster/properties` * `/api/cluster/properties/specificPropertyName` * `/api/collections` * `/api/collections/specificCollName` * `/api/collections/specificCollName/properties` * `/api/collections/specificCollName/properties/specificPropertyName` * `/api/collections/specificcollName/shards` * `/api/collections/specificCollName/shards/specificShardName` * `/api/collections/specificCollName/shards/specificShardName/replicas` * `/api/collections/specificCollName/shards/specificShardName/replicas/specificReplicaName` * `/api/collections/specificCollName/shards/specificShardName/replicas/specificReplicaName/properties` * `/api/collections/specificCollName/shards/specificShardName/replicas/specificReplicaName/properties/specificPropertyName` * `/api/configsets` * `/api/configsets/specificConfigsetName` * `/api/cores` * `/api/cores/specificCoreName` * `/api/node` === Unproxied APIs The last entry on the list above, `/api/node`, exhibits a bit of a special case. SolrCloud handles most requests as a distributed system, i.e. any request can be made to any node in the cluster and Solr will proxy or route the request internally in order to serve a response. But not all APIs work this way- some functionality is designed to only return data from the receiving node, such as `/api/node/key` which returns a cryptographic key specific to the receiving node. Solr will not proxy these requests. To represent this distinction the API design uses the idiosyncratic path `/api/node`, to help distinguish these from other node-related APIs. == HTTP Methods Where possible, HTTP methods (colloquially called 'verbs') are used semantically to distinguish between APIs available at the same path. For example, the API to delete a collection uses the `DELETE` HTTP method, as in `DELETE /api/collections/specificCollectionName`. The API to modify the collection uses the `PUT` HTTP method, as in `PUT /api/collections/specificCollectionName`. While the best effort is made to use HTTP methods semantically, the v2 API currently restricts itself to the better known HTTP methods: `GET`, `POST`, `PUT`, and `DELETE`. In some situations this leads us to eschew a more semantically appropriate verb due to its relative obscurity. The most significant example of this is the HTTP method `PATCH`, which according to the HTTP spec is used to indicate a partial update (i.e. a resource modification request which only provides the part to-be-modified). Solr's "modify collection" functionality uses partial update semantics, but the v2 API uses `PUT` instead of `PATCH` due to the relative obscurity of the latter. For use within the v2 API, the four "popular" HTTP methods have the following semantics and implications: * `GET` - used for non-mutating (i.e. "read only") requests. Most often used to list elements of a particular resource type, or fetch information about a specific named resource. * `POST` - used for non-idempotent resource modifications. * `PUT` - used for idempotent resource modifications. * `DELETE` - Used to delete or cleanup resource == Errors v2 APIs should be consistent in how they report errors. Throwing a `SolrException` will convey 1. The error code as the HTTP response status code, as `responseHeader.status` and as `error.code`, and 2. The error message as `error.msg`. API calls that reference a specific resource (e.g. `specificCollName`, `specificAliasName`, `specificPropertyName` and others per the above list) that do not exist should return `SolrException.ErrorCode.NOT_FOUND` (HTTP 404). == Exceptional Cases - "Command" APIs The pairing of semantic HTTP verbs and "resource"-based paths gives Solr an intuitive pattern for representing many operations, but not all. Many Solr APIs cover complex operations that don't map cleanly to an HTTP verb. Often these operations were initially conceived of as procedural "commands" and as such are hard to fit into the v2 APIs resource-first model. Solr's v2 API currently accommodates these "command" APIs by appending the command name (often a verb like "unload", "reload", or "split") onto the otherwise "resource"-based path. For example: Solr's core "unload" command uses the API `POST /api/cores/specificCoreName/unload`. = JAX-RS Implementation Conventions == Streaming Solr has a number of APIs that return binary file data or other arbitrary content, such as the "replication" APIs used to pull index files from other cores. Please use the following conventions when implementing similar endpoints: 1. `@Operation` annotations use an "extension property" to indicate to codegen tools that the API output is "raw" or untyped. For example: + ``` @Operation( summary = "Return the data stored in a specified ZooKeeper node", tags = {"zookeeper-read"}, extensions = { @Extension(properties = {@ExtensionProperty(name = RAW_OUTPUT_PROPERTY, value = "true")}) }) ``` 2. Interface methods should return a type that implements the JAX-RS `StreamingOutput` interface. See the `fetchFile()` method in `ReplicationApis.java` for a concrete example. == Dynamic and Partially-Dynamic Request POJOs Many Solr APIs have a request body that is fully dynamic or open-ended, with users able to specify any property name and value. In these cases, the request-body should be represented in JAX-RS by using a map: typically a `Map`. Slightly more difficult are cases where the request-body allows dynamic/open-ended fields, but also has some fields that are known statically. In these cases, developers may: 1. Represent the request-body using a POJO class. The "known" fields can be annotated with `@JsonProperty` as in standard POJOs. Dynamic fields can be allowed using Jackson's `@JsonAnyGetter` and `@JsonAnySetter` annotations as below: + ``` private Map additionalProperties = new HashMap<>(); @JsonAnyGetter public Map getAdditionalProperties() { return additionalProperties; } @JsonAnySetter public void setAdditionalProperty(String field, Object value) { additionalProperties.put(field, value); } ``` 2. Annotate the request-body parameter with the `ADDTL_FIELDS_PROPERTY` Swagger extension when declaring the request body. This tells our code-generation templates that the request-body takes additional properties the setters should be generated for. + ``` SolrJerseyResponse addField( @PathParam("fieldName") String fieldName, @RequestBody( extensions = { @Extension( properties = {@ExtensionProperty(name = ADDTL_FIELDS_PROPERTY, value = "true")}) }) AddFieldOperation requestBody) throws Exception; ``` == Response POJOs Every v2 response body extends `SolrJerseyResponse`, which provides `responseHeader` and `error`. Beyond that base, pick the narrowest existing response type that fits before writing a new one: * `SolrJerseyResponse` - plain success/error response with no extra data. The default for simple mutations. * `AsyncJerseyResponse extends SolrJerseyResponse` - adds a `requestId` field. Use this (or a subclass of it) for any API that supports the `async` request-body parameter, since `submitRemoteMessageAndHandleAsync`/`handlePotentiallyAsynchronousTask` populate `requestId` automatically when an async id is present. * `SubResponseAccumulatingJerseyResponse extends AsyncJerseyResponse` - adds `successfulSubResponsesByNodeName`, `failedSubResponsesByNodeName`, and `warning`. Use this for Overseer-driven APIs that fan out to multiple nodes/replicas (e.g. `CreateShard`, `DeleteShard`) - `AdminAPIBase.submitRemoteMessageAndHandleResponse` populates these fields from the Overseer's `success`/`failure`/`warning` NamedList entries automatically. * `FlexibleSolrJerseyResponse extends SolrJerseyResponse` - adds `@JsonAnyGetter`/`@JsonAnySetter`-backed dynamic *top-level* properties, for APIs whose entire response shape is genuinely open-ended (e.g. `SchemaDesigner`, the `Select` query API). This is different from the dynamic-request-POJO pattern above: it's for responses, and the dynamism applies to the whole top level rather than one field. If an API returns specific, known additional data beyond these bases (e.g. timing information, computed ranges, a resource's status fields), extend the appropriate base with typed `@JsonProperty` fields rather than reaching for `FlexibleSolrJerseyResponse` - see `SplitShardResponse.timing`, `SplitCoreResponse.ranges`, or `CollectionStatusResponse` for examples. Don't settle for a bare `SolrJerseyResponse` if the underlying operation actually produces more than a bare success/error - a v2 JSON caller has no other way to get that data, since (unlike v1) nothing else in the response pipeline will surface it (see the async caveat below for why this matters in practice). == Async Task Handling Solr has two distinct, non-interchangeable mechanisms for handling the `async` request parameter, depending on which base class the API extends: * **Core-level (`CoreAdminAPIBase`)**: `handlePotentiallyAsynchronousTask(response, coreName, taskId, actionName, supplier)`. If `taskId` is null, the supplier runs inline and its result is returned directly. If non-null, the supplier is wrapped in a `CoreAdminAsyncTracker.TaskObject` and submitted to `coreAdminAsyncTracker`, which tracks status for later `REQUESTSTATUS` polling. Because the supplier is a `Supplier`, it cannot throw checked exceptions directly - wrap them in `CoreAdminAPIBase.CoreAdminAPIBaseException` and rethrow, which `handlePotentiallyAsynchronousTask` unwraps back to the original checked exception for the caller. * **Collection-level (`AdminAPIBase`)**: `submitRemoteMessageAndHandleAsync`/`submitRemoteMessageAndHandleResponse(response, action, remoteMessage, asyncId[, timeoutMs])`. The `asyncId` is baked into the submitted `ZkNodeProps` message and handled by the Overseer's own async-tracking machinery; `response.requestId` is populated automatically when `asyncId` is non-null. A timeout-aware overload exists for APIs (like shard split) that legitimately need longer than `CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT` to complete. Only wrap the branches of an API that are actually meant to support async execution. A synchronous-by-design sub-operation (e.g. a "dry run" or "compute recommendations" branch that happens to share a method with the real mutating operation) should bypass async handling entirely rather than being routed through it - see the `async` caveat under "Relationship Between V1 and V2 Implementations" below for a concrete failure mode this avoids. == Relationship Between V1 and V2 Implementations Most v2 APIs have a corresponding legacy v1 API (e.g. `/admin/cores?action=RELOAD` backs `POST /api/cores/coreName/reload`). Where both exist, **the actual business logic should live in the v2 implementation class, and the v1 handler should delegate to it** - not the other way around. This convention exists for a few reasons: 1. It makes it easier to delete the v1 code down the road. 2. It prevents the v2 code from "falling behind" or being forgotten when query params or API functionality changes, since v1 traffic continuously exercises the same v2 code path. 3. It gives the v2 endpoint test coverage "for free" - most of Solr's existing test suite targets v1 APIs, so having v1 call v2 gives confidence that the v2 endpoint works correctly even in the absence of direct v2-specific tests. How this delegation is implemented differs depending on whether the API executes synchronously or is processed by the Overseer. === Synchronous (core-level) APIs For `CoreAdminAPIBase`-derived APIs (i.e. `/api/cores/...`), the v1 `CoreAdminHandler.CoreAdminOp` implementation is reduced to a thin adapter: parse the v1 `SolrParams` into the v2 request-body POJO, construct the v2 API class, call it, and squash the response back into the v1 `NamedList` via `V2ApiUtils.squashIntoSolrResponseWithoutHeader`. ```java class MergeIndexesOp implements CoreAdminHandler.CoreAdminOp { @Override public void execute(CoreAdminHandler.CallInfo it) throws Exception { SolrParams params = it.req.getParams(); final var requestBody = new MergeIndexesRequestBody(); // ... populate requestBody from params ... final var mergeIndexesApi = new MergeIndexes(it.handler.coreContainer, it.handler.coreAdminAsyncTracker, it.req, it.rsp); final var response = mergeIndexesApi.mergeIndexes(cname, requestBody); V2ApiUtils.squashIntoSolrResponseWithoutHeader(it.rsp, response); } } ``` See `RenameCore`/`CoreAdminOperation.RENAME_OP`, `MergeIndexes`/`MergeIndexesOp`, and `SplitCoreAPI`/`SplitOp` for examples of this pattern. === Overseer-driven (collection-level) APIs For `AdminAPIBase`-derived APIs (i.e. `/api/collections/...`) that are processed asynchronously by the Overseer via a ZK-based command queue, the underlying `cloud/api/collections/*Cmd.java` executor (e.g. `SplitShardCmd`, `CreateShardCmd`) is *not* moved into v2 - it stays registered in `CollApiCmds.java` and continues to execute on the Overseer thread exactly as before. "Logic in v2" here means the v2 class is responsible for: 1. Validating parameters (including any cross-parameter conflict checks previously done by the v1 handler, so the check applies uniformly to v1 and native v2 requests) 2. Building the `ZkNodeProps` "remote message" the Overseer command expects 3. Submitting it via `AdminAPIBase.submitRemoteMessageAndHandleResponse`/`submitRemoteMessageAndHandleAsync` - the same ZK-queue submission machinery v1 always used The v1 `CollectionOperation` enum entry is then reduced to a one-line delegation: ```java SPLITSHARD_OP( SPLITSHARD, (req, rsp, h) -> { SplitShardAPI.invokeWithV1Params(h.coreContainer, req, rsp); return null; }), ``` Returning `null` from a `CollectionOperation` lambda tells `CollectionsHandler.invokeAction()` that the request has already been fully handled. Returning a non-null `Map` instead falls back to the older pattern where `CollectionsHandler` itself builds and submits the `ZkNodeProps` message generically - this is still the state of many not-yet-migrated v1 actions, and is *not* an example to follow for new migrations. See `CreateShard`/`CREATESHARD_OP`, `DeleteShard`/`DELETESHARD_OP`, and `SplitShardAPI`/`SPLITSHARD_OP` for examples of the target pattern. === Standard v1-delegation entry points By convention, a migrated v2 API class exposes two `public static` methods for v1 to call: * `createRequestBodyFromV1Params(SolrParams params)` - parses legacy params into the v2 request-body POJO. * `invokeWithV1Params(CoreContainer, SolrQueryRequest, SolrQueryResponse)` (sometimes named `invokeFromV1Params` - naming isn't fully consistent yet) - constructs the v2 API class, calls it, and squashes the response into the v1 `SolrQueryResponse`. === Don't forward `async` from v1 `CoreAdminHandler.handleRequestBody()` and `CollectionsHandler.invokeAction()` already wrap the *entire* v1 dispatch in their own async-task submission, keyed by the request's `async` parameter, before the `CoreAdminOp`/`CollectionOperation` even runs. If a v1 adapter also copies that same `async` value into the v2 request body, the v2 class's own async handling (`handlePotentiallyAsynchronousTask`, `submitRemoteMessageAndHandleAsync`) will try to register the identical task id a second time, and fail with `Duplicate request with the same requestid found.` *Leave the v2 request body's `async` field unset when delegating from v1.* `MergeIndexesRequestBody` has no `async` field at all, and `SplitOp`'s v1 adapter deliberately omits setting one - both are correct examples of this. This only applies to genuine v1-delegation adapters; native v2 JSON requests should set `async` normally, and the v2 class's own handling of it is unaffected. Watch for this same class of bug in *internal* sub-requests too: some Overseer commands (e.g. `SplitShardCmd`'s `splitByPrefix` sub-request, sent via `ShardRequestTracker.sendShardRequest`) piggyback an internal correlation id on the `async` param purely for response tracking, not because the sub-request is meant to be tracked as a real async task. If a migrated v2 method routes such a sub-request through its own async wrapper, the same "duplicate requestid" failure results. Keep any such synchronous-by-design branches (e.g. a `getRanges=true` calculation) entirely outside the async wrapper, matching the original v1 code's control flow rather than funneling every branch through the same async-handling path. == Testing V2 APIs Newer v2 API tests favor exercising the real HTTP stack over mocking it: 1. Start a real embedded Solr instance with `SolrJettyTestRule` (a `@ClassRule`) rather than the older `initCore()`/mock-`SolrQueryRequest` style. 2. Issue requests through the *generated* SolrJ client (e.g. `CoresApi.SplitCore`, `ShardsApi.SplitShard`) rather than hand-built `GenericV2SolrRequest`s, so the test also exercises the client codegen. 3. Give each API its own dedicated test class under `handler.admin.api` (e.g. `RenameCoreAPITest`, `SplitCoreAPITest`, `SplitShardAPITest`), rather than adding cases to a large shared mapping test. 4. Assert error responses via `expectThrows(RemoteSolrException.class, () -> request.process(client))`, then check `.code()` and `.getMessage()` - a non-2xx v2 response surfaces as a thrown exception on the client side, not as a populated `.error` field on a normally-returned response object. This has superseded the older `V2ApiMappingTest`-based style (e.g. `V2CoreAPIMappingTest`), which mocks the v1 handler and asserts on the `SolrParams` the v2-to-v1 conversion produces. That style still exists for APIs that haven't been migrated to the "logic lives in v2" pattern yet, but shouldn't be extended for newly-migrated APIs - once an API's logic moves into v2, its mapping test loses its reason to exist and should be removed (see "Relationship Between V1 and V2 Implementations" above) in favor of a real-client test. One exception: white-box tests that need access to package-private helper methods (e.g. `SplitHandlerTest`, which tests `SplitCoreAPI`'s histogram/range-recommendation internals) should live in the *same package* as the class under test, and move with it if that class's package ever changes.