From e83537d81b2e0a09a625ae6f7b9efe29d03d1c40 Mon Sep 17 00:00:00 2001
From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com>
Date: Thu, 22 Oct 2026 15:39:10 +0000
Subject: [PATCH 1/3] [fern-generated] Update SDK
Generated by Fern
CLI Version: unknown
Generators:
- fernapi/fern-java-sdk: 4.32.4
---
.fern/metadata.json | 8 +-
.github/workflows/ci.yml | 2 +-
README.md | 27 +-
build.gradle | 4 +-
.../catchall/core/BodyProperties.java | 89 ++++
.../catchall/core/ClientOptions.java | 4 +-
.../newscatcher/catchall/core/LogLevel.java | 4 +-
.../catchall/core/LoggingInterceptor.java | 5 +-
.../catchall/core/PathSegments.java | 21 +
.../catchall/core/RequestOptions.java | 35 +-
.../catchall/core/RetryInterceptor.java | 239 +++++++++-
.../catchall/core/SseEventParser.java | 36 ++
.../com/newscatcher/catchall/core/Stream.java | 52 +-
.../datasets/AsyncDatasetsClient.java | 450 +++++++++++++++---
.../datasets/AsyncRawDatasetsClient.java | 259 ++++++++--
.../resources/datasets/DatasetsClient.java | 24 +-
.../resources/datasets/RawDatasetsClient.java | 108 ++++-
.../requests/AddEntitiesToDatasetRequest.java | 9 +-
.../requests/CreateDatasetFromCsvRequest.java | 9 +-
.../requests/CreateDatasetRequest.java | 11 +-
.../requests/DeleteDatasetRequest.java | 6 +-
.../datasets/requests/GetDatasetRequest.java | 6 +-
.../GetDatasetStatusHistoryRequest.java | 6 +-
.../requests/ListDatasetEntitiesRequest.java | 15 +-
.../requests/ListDatasetsRequest.java | 16 +-
.../RemoveEntitiesFromDatasetRequest.java | 9 +-
.../requests/UpdateDatasetRequest.java | 9 +-
.../requests/UploadCsvToDatasetRequest.java | 6 +-
.../entities/AsyncEntitiesClient.java | 195 +++++++-
.../entities/AsyncRawEntitiesClient.java | 73 ++-
.../resources/entities/RawEntitiesClient.java | 25 +-
.../requests/CreateEntitiesBatchRequest.java | 9 +-
.../requests/DeleteEntityRequest.java | 6 +-
.../entities/requests/GetEntityRequest.java | 6 +-
.../requests/ListEntitiesRequest.java | 16 +-
.../requests/UpdateEntityRequest.java | 12 +-
.../AsyncEventMonitorsClient.java | 387 +++++++++++++--
.../AsyncRawEventMonitorsClient.java | 121 ++++-
.../eventmonitors/RawEventMonitorsClient.java | 41 +-
.../requests/CreateMonitorRequestDto.java | 15 +-
.../requests/DeleteMonitorRequest.java | 6 +-
.../requests/DisableMonitorRequest.java | 6 +-
.../requests/EnableMonitorRequestDto.java | 9 +-
.../GetMonitorStatusHistoryRequest.java | 6 +-
.../requests/ListMonitorJobsRequest.java | 9 +-
.../requests/ListMonitorsRequest.java | 13 +-
.../PullMonitorResultsCsvRequest.java | 6 +-
.../requests/PullMonitorResultsRequest.java | 6 +-
.../requests/UpdateMonitorRequestDto.java | 12 +-
.../types/DisableMonitorResponse.java | 11 +-
.../types/EnableMonitorResponse.java | 11 +-
.../types/ListMonitorJobsResponse.java | 12 +-
.../resources/jobs/AsyncJobsClient.java | 313 ++++++++++--
.../resources/jobs/AsyncRawJobsClient.java | 111 ++++-
.../resources/jobs/RawJobsClient.java | 31 +-
.../jobs/requests/ContinueRequestDto.java | 9 +-
.../jobs/requests/DeleteJobRequest.java | 6 +-
.../requests/GetJobResultsCsvRequest.java | 6 +-
.../jobs/requests/GetJobResultsRequest.java | 9 +-
.../jobs/requests/GetJobStatusRequest.java | 6 +-
.../jobs/requests/GetUserJobsRequest.java | 14 +-
.../jobs/requests/InitializeRequestDto.java | 12 +-
.../requests/ListSourceGroupsRequest.java | 9 +-
.../jobs/requests/SubmitRequestDto.java | 23 +-
.../requests/ValidateQueryRequestDto.java | 9 +-
.../resources/meta/AsyncMetaClient.java | 58 ++-
.../resources/meta/AsyncRawMetaClient.java | 31 +-
.../resources/meta/RawMetaClient.java | 7 +-
.../meta/types/GetVersionResponse.java | 9 +-
.../meta/types/HealthCheckResponse.java | 9 +-
.../projects/AsyncProjectsClient.java | 331 +++++++++++--
.../projects/AsyncRawProjectsClient.java | 114 ++++-
.../resources/projects/RawProjectsClient.java | 42 +-
.../requests/AddResourceRequestDto.java | 9 +-
.../requests/CreateProjectRequestDto.java | 9 +-
.../requests/DeleteProjectRequest.java | 9 +-
.../requests/GetProjectOverviewRequest.java | 6 +-
.../projects/requests/GetProjectRequest.java | 6 +-
.../requests/ListProjectResourcesRequest.java | 11 +-
.../requests/ListProjectsRequest.java | 12 +-
.../RemoveResourceFromProjectRequest.java | 6 +-
.../requests/UpdateProjectRequestDto.java | 9 +-
.../webhooks/AsyncRawWebhooksClient.java | 156 ++++--
.../webhooks/AsyncWebhooksClient.java | 418 +++++++++++++---
.../resources/webhooks/RawWebhooksClient.java | 60 ++-
.../AssignWebhookResourceRequestDto.java | 9 +-
.../requests/CreateWebhookRequestDto.java | 18 +-
.../requests/DeleteWebhookRequest.java | 6 +-
.../GetWebhookDeliveryHistoryRequest.java | 12 +-
.../webhooks/requests/GetWebhookRequest.java | 6 +-
.../requests/ListWebhookResourcesRequest.java | 11 +-
.../ListWebhooksForResourceRequest.java | 11 +-
.../requests/ListWebhooksRequest.java | 12 +-
.../RemoveWebhookResourceRequest.java | 6 +-
.../requests/TestWebhookRequestDto.java | 9 +-
.../requests/TriggerWebhookRequest.java | 9 +-
.../requests/UpdateWebhookRequestDto.java | 18 +-
.../types/AddResourceResponseDto.java | 9 +-
.../catchall/types/AdditionalAttributes.java | 9 +-
.../catchall/types/ApiKeyAuthDto.java | 9 +-
.../catchall/types/ApiKeyAuthResponseDto.java | 9 +-
.../AssignWebhookResourceResponseDto.java | 12 +-
.../catchall/types/BaseRecord.java | 11 +-
.../catchall/types/BaseRecordEnrichment.java | 9 +-
.../catchall/types/BasicAuthDto.java | 9 +-
.../catchall/types/BasicAuthResponseDto.java | 9 +-
.../catchall/types/BearerAuthDto.java | 9 +-
.../catchall/types/BearerAuthResponseDto.java | 9 +-
.../newscatcher/catchall/types/Citation.java | 11 +-
.../catchall/types/CompanyAttributes.java | 11 +-
.../types/CompanyEnrichmentOutput.java | 11 +-
.../CompanyEnrichmentOutputMetadata.java | 11 +-
.../catchall/types/ConnectedDataset.java | 9 +-
.../catchall/types/ConnectedEntity.java | 16 +-
.../catchall/types/ContinueResponseDto.java | 12 +-
.../types/CreateDatasetCsvResponse.java | 12 +-
.../types/CreateEntitiesBatchResponse.java | 9 +-
.../catchall/types/CreateEntityRequest.java | 13 +-
.../catchall/types/CreateEntityResponse.java | 9 +-
.../types/CreateMonitorResponseDto.java | 9 +-
.../types/CreateProjectResponseDto.java | 12 +-
.../types/CreateWebhookResponseDto.java | 9 +-
.../types/DatasetEntityIdsRequest.java | 9 +-
.../types/DatasetEntityListResponse.java | 12 +-
.../catchall/types/DatasetListResponse.java | 12 +-
.../catchall/types/DatasetResponse.java | 20 +-
.../catchall/types/DatasetStatusEntry.java | 11 +-
.../types/DatasetStatusHistoryResponse.java | 9 +-
.../catchall/types/DeleteJobResponseDto.java | 9 +-
.../types/DeleteMonitorResponseDto.java | 11 +-
.../types/DeliveryHistoryItemDto.java | 19 +-
.../types/DeliveryHistoryResponseDto.java | 15 +-
.../catchall/types/EnrichmentSchema.java | 9 +-
.../catchall/types/EntityListResponse.java | 12 +-
.../catchall/types/EntityResponse.java | 19 +-
.../catchall/types/EntitySummary.java | 15 +-
.../types/EntityValidationErrorBody.java | 11 +-
.../com/newscatcher/catchall/types/Error.java | 9 +-
.../newscatcher/catchall/types/EventTags.java | 9 +-
.../catchall/types/FormatterConfigDto.java | 9 +-
.../types/GetPlanLimitsResponseDto.java | 9 +-
.../catchall/types/GetWebhookResponseDto.java | 9 +-
.../catchall/types/InitializeResponseDto.java | 15 +-
.../newscatcher/catchall/types/JobStep.java | 9 +-
.../types/ListMonitorsResponseDto.java | 13 +-
.../types/ListSourceGroupsResponseDto.java | 12 +-
.../types/ListUserJobsResponseDto.java | 13 +-
.../ListWebhookResourcesResponseDto.java | 13 +-
.../types/ListWebhooksResponseDto.java | 13 +-
.../types/ManageEntitiesResponse.java | 9 +-
.../catchall/types/MonitorCitation.java | 14 +-
.../catchall/types/MonitorJobItem.java | 11 +-
.../catchall/types/MonitorListItemDto.java | 19 +-
.../catchall/types/MonitorRecord.java | 14 +-
.../catchall/types/MonitorStatusEntry.java | 11 +-
.../MonitorStatusHistoryResponseDto.java | 13 +-
.../catchall/types/PlanFeature.java | 13 +-
.../types/ProjectListResponseDto.java | 13 +-
.../types/ProjectOverviewResponseDto.java | 9 +-
.../ProjectOverviewResponseDtoOverview.java | 13 +-
.../catchall/types/ProjectResourceDto.java | 13 +-
.../types/ProjectResourceListResponseDto.java | 13 +-
.../catchall/types/ProjectResponseDto.java | 16 +-
.../catchall/types/ProjectSummaryDto.java | 14 +-
.../catchall/types/PullJobResponseDto.java | 30 +-
.../types/PullJobResponseDtoDateRange.java | 9 +-
.../types/PullMonitorResponseDto.java | 19 +-
.../types/PullMonitorResponseDtoRunInfo.java | 9 +-
.../newscatcher/catchall/types/Record.java | 14 +-
.../catchall/types/ReferenceJob.java | 11 +-
.../types/RemoveResourceResponseDto.java | 9 +-
.../catchall/types/ResourceItemDto.java | 9 +-
.../catchall/types/ResourceResultDto.java | 13 +-
.../catchall/types/SharingInfo.java | 11 +-
.../catchall/types/SkippedRow.java | 9 +-
.../catchall/types/SourceGroupRef.java | 9 +-
.../catchall/types/StatusResponseDto.java | 9 +-
.../catchall/types/SubmitResponseDto.java | 9 +-
.../catchall/types/Suggestion.java | 9 +-
.../types/TestWebhookResponseDto.java | 12 +-
.../types/UpdateMonitorResponseDto.java | 9 +-
.../types/UpdateProjectResponseDto.java | 11 +-
.../types/UpdateWebhookResponseDto.java | 9 +-
.../types/UploadCsvToDatasetResponse.java | 11 +-
.../newscatcher/catchall/types/UserJob.java | 15 +-
.../types/ValidateQueryResponseDto.java | 14 +-
.../catchall/types/ValidationErrorDetail.java | 9 +-
.../types/ValidationErrorResponse.java | 9 +-
.../catchall/types/ValidationReport.java | 12 +-
.../catchall/types/ValidatorSchema.java | 9 +-
.../catchall/types/WebhookDto.java | 13 +-
.../catchall/types/WebhookPayload.java | 17 +-
.../WebhookResourceMappingResponseDto.java | 13 +-
.../catchall/types/WebhookResponseDto.java | 23 +-
.../catchall/core/PathSegmentsTest.java | 36 ++
195 files changed, 4945 insertions(+), 714 deletions(-)
create mode 100644 src/main/java/com/newscatcher/catchall/core/BodyProperties.java
create mode 100644 src/main/java/com/newscatcher/catchall/core/PathSegments.java
create mode 100644 src/test/java/com/newscatcher/catchall/core/PathSegmentsTest.java
diff --git a/.fern/metadata.json b/.fern/metadata.json
index 104acf4..e1eaff9 100644
--- a/.fern/metadata.json
+++ b/.fern/metadata.json
@@ -1,16 +1,16 @@
{
- "cliVersion": "5.143.0",
+ "cliVersion": "5.170.5",
"generatorName": "fernapi/fern-java-sdk",
- "generatorVersion": "4.23.1",
+ "generatorVersion": "4.32.4",
"generatorConfig": {
"publish-to": "central",
"client-class-name": "CatchAllApi",
"package-prefix": "com.newscatcher.catchall"
},
- "originGitCommit": "6ef7448ab601e5b372caec45717b0390a4150d2c",
+ "originGitCommit": "b7764dca3eb740a480b120edb87932f928b9210e",
"originGitCommitIsDirty": true,
"invokedBy": "ci",
"requestedVersion": "AUTO",
"ciProvider": "unknown",
- "sdkVersion": "3.0.5"
+ "sdkVersion": "0.0.0-fern-placeholder"
}
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1f32646..7b513f7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -62,7 +62,7 @@ jobs:
env:
MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- MAVEN_PUBLISH_REGISTRY_URL: "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
+ MAVEN_PUBLISH_REGISTRY_URL: "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/"
MAVEN_SIGNATURE_KID: ${{ secrets.MAVEN_SIGNATURE_KID }}
MAVEN_SIGNATURE_SECRET_KEY: ${{ secrets.MAVEN_SIGNATURE_SECRET_KEY }}
MAVEN_SIGNATURE_PASSWORD: ${{ secrets.MAVEN_SIGNATURE_PASSWORD }}
diff --git a/README.md b/README.md
index a90bbfd..1baa2be 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ The Newscatcher Java library provides convenient access to the Newscatcher APIs
- [Retries](#retries)
- [Timeouts](#timeouts)
- [Custom Headers](#custom-headers)
+ - [Additional Body Properties](#additional-body-properties)
- [Access Raw Response Data](#access-raw-response-data)
- [Contributing](#contributing)
- [Support](#support)
@@ -35,7 +36,7 @@ Add the dependency in your `build.gradle` file:
```groovy
dependencies {
- implementation 'com.newscatcherapi:newscatcher-catchall-sdk:3.0.5'
+ implementation 'com.newscatcherapi:newscatcher-catchall-sdk:0.0.0-fern-placeholder'
}
```
@@ -47,7 +48,7 @@ Add the dependency in your `pom.xml` file:
com.newscatcherapi
newscatcher-catchall-sdk
- 3.0.5
+ 0.0.0-fern-placeholder
```
@@ -99,7 +100,7 @@ import com.newscatcher.catchall.core.Environment;
CatchAllApi client = CatchAllApi
.builder()
- .environment(Environment.Default)
+ .environment(Environment.DEFAULT)
.build();
```
@@ -231,6 +232,25 @@ client.jobs().createJob(
);
```
+### Additional Body Properties
+
+If you need to send a request body property that is not yet part of the SDK (for example, a beta field),
+use the `addBodyProperty` request option. Properties are sent with the key exactly as given and override
+any property with the same key; for endpoints without a request body, they are sent as a JSON body.
+This is supported for JSON and form-urlencoded request bodies, but not for multipart (file upload) requests.
+
+```java
+import com.newscatcher.catchall.core.RequestOptions;
+
+client.jobs().createJob(
+ ...,
+ RequestOptions
+ .builder()
+ .addBodyProperty("extra_field", "extra-value")
+ .build()
+);
+```
+
### Access Raw Response Data
The SDK provides access to raw response data, including headers, through the `withRawResponse()` method.
@@ -268,3 +288,4 @@ On the other hand, contributions to the README are always very welcome!
+
diff --git a/build.gradle b/build.gradle
index 20f4a1b..50f8a68 100644
--- a/build.gradle
+++ b/build.gradle
@@ -57,7 +57,7 @@ java {
group = 'com.newscatcherapi'
-version = '3.0.5'
+version = '0.0.0-fern-placeholder'
jar {
dependsOn(":generatePomFileForMavenPublication")
@@ -88,7 +88,7 @@ publishing {
maven(MavenPublication) {
groupId = 'com.newscatcherapi'
artifactId = 'newscatcher-catchall-sdk'
- version = '3.0.5'
+ version = '0.0.0-fern-placeholder'
from components.java
pom {
name = 'newscatcher'
diff --git a/src/main/java/com/newscatcher/catchall/core/BodyProperties.java b/src/main/java/com/newscatcher/catchall/core/BodyProperties.java
new file mode 100644
index 0000000..50ee2eb
--- /dev/null
+++ b/src/main/java/com/newscatcher/catchall/core/BodyProperties.java
@@ -0,0 +1,89 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.newscatcher.catchall.core;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import okhttp3.RequestBody;
+
+/**
+ * Merges the additional body properties set through {@code RequestOptions.Builder#addBodyProperty} into request
+ * bodies. Properties are sent with the keys exactly as given (no casing transform) and override any generated body
+ * property with the same key.
+ *
+ *
Supported for JSON and {@code application/x-www-form-urlencoded} bodies. Multipart (file upload) and raw bytes
+ * bodies are sent unchanged.
+ */
+public final class BodyProperties {
+
+ private BodyProperties() {}
+
+ public static boolean isPresent(Map bodyProperties) {
+ return bodyProperties != null && !bodyProperties.isEmpty();
+ }
+
+ /**
+ * Returns the value to serialize as the JSON request body. Without body properties the body is returned unchanged.
+ * Otherwise the body is converted to a JSON object (an empty one when the body is null or absent) and the body
+ * properties are set on it, overriding existing keys. Bodies that are not JSON objects (arrays, primitives) are
+ * returned unchanged.
+ */
+ public static Object merge(Object body, Map bodyProperties) throws JsonProcessingException {
+ if (!isPresent(bodyProperties)) {
+ return body;
+ }
+ JsonNode node;
+ try {
+ node = ObjectMappers.JSON_MAPPER.valueToTree(body);
+ } catch (IllegalArgumentException e) {
+ throw JsonMappingException.from((com.fasterxml.jackson.core.JsonParser) null, e.getMessage(), e);
+ }
+ ObjectNode merged;
+ if (node == null || node.isNull() || node.isMissingNode()) {
+ merged = ObjectMappers.JSON_MAPPER.createObjectNode();
+ } else if (node.isObject()) {
+ merged = (ObjectNode) node;
+ } else {
+ return body;
+ }
+ for (Map.Entry entry : bodyProperties.entrySet()) {
+ merged.putPOJO(entry.getKey(), entry.getValue());
+ }
+ return merged;
+ }
+
+ /**
+ * Returns a JSON request body made of the body properties for an endpoint without a request body, or the fallback
+ * when there are no body properties.
+ */
+ public static RequestBody toRequestBody(Map bodyProperties, RequestBody fallback) {
+ if (!isPresent(bodyProperties)) {
+ return fallback;
+ }
+ try {
+ return RequestBody.create(
+ ObjectMappers.JSON_MAPPER.writeValueAsBytes(merge(null, bodyProperties)),
+ MediaTypes.APPLICATION_JSON);
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Failed to serialize body properties", e);
+ }
+ }
+
+ /** Returns the form parameters with the body properties added, overriding existing keys. */
+ public static Map mergeFormParams(
+ Map formParams, Map bodyProperties) {
+ Map merged = new LinkedHashMap<>();
+ if (formParams != null) {
+ merged.putAll(formParams);
+ }
+ if (isPresent(bodyProperties)) {
+ merged.putAll(bodyProperties);
+ }
+ return merged;
+ }
+}
diff --git a/src/main/java/com/newscatcher/catchall/core/ClientOptions.java b/src/main/java/com/newscatcher/catchall/core/ClientOptions.java
index a831b43..3aa651e 100644
--- a/src/main/java/com/newscatcher/catchall/core/ClientOptions.java
+++ b/src/main/java/com/newscatcher/catchall/core/ClientOptions.java
@@ -60,10 +60,10 @@ private ClientOptions(
this.headers.putAll(headers);
this.headers.putAll(new HashMap() {
{
- put("User-Agent", "com.newscatcherapi.newscatcher-catchall-sdk/3.0.5");
+ put("User-Agent", "com.newscatcherapi.newscatcher-catchall-sdk/0.0.0-fern-placeholder");
put("X-Fern-Language", "JAVA");
put("X-Fern-SDK-Name", "com.newscatcherapi:newscatcher-catchall-sdk");
- put("X-Fern-SDK-Version", "3.0.5");
+ put("X-Fern-SDK-Version", "0.0.0-fern-placeholder");
}
});
this.headerSuppliers = headerSuppliers;
diff --git a/src/main/java/com/newscatcher/catchall/core/LogLevel.java b/src/main/java/com/newscatcher/catchall/core/LogLevel.java
index 488b552..1cdc38a 100644
--- a/src/main/java/com/newscatcher/catchall/core/LogLevel.java
+++ b/src/main/java/com/newscatcher/catchall/core/LogLevel.java
@@ -3,6 +3,8 @@
*/
package com.newscatcher.catchall.core;
+import java.util.Locale;
+
/**
* Log levels for SDK logging configuration.
* Silent by default — no log output unless explicitly configured.
@@ -31,6 +33,6 @@ public int getValue() {
* @throws IllegalArgumentException if the string does not match any level
*/
public static LogLevel fromString(String level) {
- return LogLevel.valueOf(level.toUpperCase());
+ return LogLevel.valueOf(level.toUpperCase(Locale.ROOT));
}
}
diff --git a/src/main/java/com/newscatcher/catchall/core/LoggingInterceptor.java b/src/main/java/com/newscatcher/catchall/core/LoggingInterceptor.java
index 4cf746e..eef921a 100644
--- a/src/main/java/com/newscatcher/catchall/core/LoggingInterceptor.java
+++ b/src/main/java/com/newscatcher/catchall/core/LoggingInterceptor.java
@@ -6,6 +6,7 @@
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
+import java.util.Locale;
import java.util.Set;
import okhttp3.Interceptor;
import okhttp3.Request;
@@ -58,7 +59,7 @@ public Response intercept(Chain chain) throws IOException {
sb.append(", ");
}
sb.append(name).append("=");
- if (SENSITIVE_HEADERS.contains(name.toLowerCase())) {
+ if (SENSITIVE_HEADERS.contains(name.toLowerCase(Locale.ROOT))) {
sb.append("[REDACTED]");
} else {
sb.append(request.header(name));
@@ -83,7 +84,7 @@ public Response intercept(Chain chain) throws IOException {
sb.append(", ");
}
sb.append(name).append("=");
- if (SENSITIVE_HEADERS.contains(name.toLowerCase())) {
+ if (SENSITIVE_HEADERS.contains(name.toLowerCase(Locale.ROOT))) {
sb.append("[REDACTED]");
} else {
sb.append(response.header(name));
diff --git a/src/main/java/com/newscatcher/catchall/core/PathSegments.java b/src/main/java/com/newscatcher/catchall/core/PathSegments.java
new file mode 100644
index 0000000..01726ea
--- /dev/null
+++ b/src/main/java/com/newscatcher/catchall/core/PathSegments.java
@@ -0,0 +1,21 @@
+/**
+ * This file was auto-generated by Fern from our API Definition.
+ */
+package com.newscatcher.catchall.core;
+
+public final class PathSegments {
+
+ private PathSegments() {}
+
+ /**
+ * Returns the value unchanged, rejecting "." and "..". These are dot-segments that URL builders resolve,
+ * which would otherwise change the endpoint a request is sent to.
+ */
+ public static String validate(String value) {
+ if (".".equals(value) || "..".equals(value)) {
+ throw new IllegalArgumentException(
+ "Invalid path parameter value \"" + value + "\": \".\" and \"..\" are not allowed.");
+ }
+ return value;
+ }
+}
diff --git a/src/main/java/com/newscatcher/catchall/core/RequestOptions.java b/src/main/java/com/newscatcher/catchall/core/RequestOptions.java
index a556072..1e0dedb 100644
--- a/src/main/java/com/newscatcher/catchall/core/RequestOptions.java
+++ b/src/main/java/com/newscatcher/catchall/core/RequestOptions.java
@@ -4,6 +4,7 @@
package com.newscatcher.catchall.core;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@@ -26,6 +27,8 @@ public final class RequestOptions {
private final Map> queryParameterSuppliers;
+ private final Map bodyProperties;
+
private RequestOptions(
String apiKey,
Optional timeout,
@@ -34,7 +37,8 @@ private RequestOptions(
Map headers,
Map> headerSuppliers,
Map queryParameters,
- Map> queryParameterSuppliers) {
+ Map> queryParameterSuppliers,
+ Map bodyProperties) {
this.apiKey = apiKey;
this.timeout = timeout;
this.timeoutTimeUnit = timeoutTimeUnit;
@@ -43,6 +47,7 @@ private RequestOptions(
this.headerSuppliers = headerSuppliers;
this.queryParameters = queryParameters;
this.queryParameterSuppliers = queryParameterSuppliers;
+ this.bodyProperties = bodyProperties;
}
public Optional getTimeout() {
@@ -77,6 +82,13 @@ public Map getQueryParameters() {
return queryParameters;
}
+ /**
+ * Additional properties merged into the request body, keyed by their wire names.
+ */
+ public Map getBodyProperties() {
+ return new LinkedHashMap<>(this.bodyProperties);
+ }
+
public static Builder builder() {
return new Builder();
}
@@ -98,6 +110,8 @@ public static class Builder {
private final Map> queryParameterSuppliers = new HashMap<>();
+ private final Map bodyProperties = new LinkedHashMap<>();
+
public Builder apiKey(String apiKey) {
this.apiKey = apiKey;
return this;
@@ -139,6 +153,22 @@ public Builder addQueryParameter(String key, Supplier value) {
return this;
}
+ /**
+ * Adds a property to the JSON or form-urlencoded request body, keyed by its wire name. It overrides any request body property with the same key, and is sent as the whole body for endpoints without one. Multipart (file upload) bodies are not supported.
+ */
+ public Builder addBodyProperty(String key, Object value) {
+ this.bodyProperties.put(key, value);
+ return this;
+ }
+
+ /**
+ * Adds each entry as a request body property. See {@link #addBodyProperty}.
+ */
+ public Builder bodyProperties(Map bodyProperties) {
+ this.bodyProperties.putAll(bodyProperties);
+ return this;
+ }
+
public RequestOptions build() {
return new RequestOptions(
apiKey,
@@ -148,7 +178,8 @@ public RequestOptions build() {
headers,
headerSuppliers,
queryParameters,
- queryParameterSuppliers);
+ queryParameterSuppliers,
+ bodyProperties);
}
}
}
diff --git a/src/main/java/com/newscatcher/catchall/core/RetryInterceptor.java b/src/main/java/com/newscatcher/catchall/core/RetryInterceptor.java
index 783accd..bf208f2 100644
--- a/src/main/java/com/newscatcher/catchall/core/RetryInterceptor.java
+++ b/src/main/java/com/newscatcher/catchall/core/RetryInterceptor.java
@@ -4,14 +4,20 @@
package com.newscatcher.catchall.core;
import java.io.IOException;
+import java.io.InterruptedIOException;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Optional;
import java.util.Random;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
import okhttp3.Call;
+import okhttp3.Callback;
import okhttp3.Interceptor;
+import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
@@ -63,6 +69,10 @@ public RetryInterceptor(
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
+ if (request.tag(AsyncAttempt.class) != null) {
+ // Retries of calls created by newAsyncCall are scheduled off OkHttp's dispatcher threads.
+ return chain.proceed(request);
+ }
int effectiveMaxRetries = resolveMaxRetries(request);
Response response = chain.proceed(request);
@@ -88,7 +98,11 @@ private Response retryChain(Response response, Chain chain, int maxRetries) thro
Thread.sleep(nextBackoff.get().toMillis());
} catch (InterruptedException e) {
response.close();
- throw new IOException("Interrupted while trying request", e);
+ Thread.currentThread().interrupt();
+ InterruptedIOException interrupted =
+ new InterruptedIOException("Interrupted while waiting to retry request");
+ interrupted.initCause(e);
+ throw interrupted;
} finally {
callTimeout.ifPresent(AsyncTimeout::enter);
}
@@ -114,6 +128,229 @@ private Response retryChain(Response response, Chain chain, int maxRetries) thro
return response;
}
+ /**
+ * Creates a call for the async client. If {@code client} has a {@link RetryInterceptor}, retries are scheduled
+ * after the backoff delay instead of sleeping inside the interceptor, so a call that is waiting to retry does not
+ * hold one of OkHttp's dispatcher slots (5 per host by default) and stops waiting as soon as it is cancelled.
+ */
+ public static AsyncCall newAsyncCall(OkHttpClient client, Request request) {
+ for (Interceptor interceptor : client.interceptors()) {
+ if (interceptor instanceof RetryInterceptor) {
+ RetryInterceptor retryInterceptor = (RetryInterceptor) interceptor;
+ return new AsyncRetryingCall(
+ retryInterceptor, client, request, retryInterceptor.resolveMaxRetries(request));
+ }
+ }
+ return new AsyncRetryingCall(null, client, request, 0);
+ }
+
+ /** An async OkHttp call that retries retryable responses without blocking a dispatcher thread. */
+ public interface AsyncCall {
+ void enqueue(Callback callback);
+
+ void cancel();
+ }
+
+ /** Marks an attempt of an {@link AsyncCall} so that {@link #intercept(Chain)} does not retry it in place. */
+ private static final class AsyncAttempt {
+ private static final AsyncAttempt INSTANCE = new AsyncAttempt();
+ }
+
+ private static final class RetryScheduler {
+ private static final ScheduledThreadPoolExecutor INSTANCE = create();
+
+ private static ScheduledThreadPoolExecutor create() {
+ ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, runnable -> {
+ Thread thread = new Thread(runnable, "RetryInterceptor-scheduler");
+ thread.setDaemon(true);
+ return thread;
+ });
+ executor.setRemoveOnCancelPolicy(true);
+ return executor;
+ }
+ }
+
+ private static final class AsyncRetryingCall implements AsyncCall, Callback {
+ private final OkHttpClient client;
+ private final Request request;
+ private final ExponentialBackoff backoff;
+ private final Object lock = new Object();
+
+ // All fields below are guarded by lock.
+ private Callback callback;
+ private Call currentCall;
+ private ScheduledFuture> pendingRetry;
+ private Response previousResponse;
+ private boolean executed;
+ private boolean canceled;
+ private boolean completed;
+
+ AsyncRetryingCall(RetryInterceptor retryInterceptor, OkHttpClient client, Request request, int maxRetries) {
+ this.client = client;
+ this.request = request.newBuilder()
+ .tag(AsyncAttempt.class, AsyncAttempt.INSTANCE)
+ .build();
+ this.backoff = retryInterceptor != null ? retryInterceptor.new ExponentialBackoff(maxRetries) : null;
+ }
+
+ @Override
+ public void enqueue(Callback callback) {
+ Call call;
+ boolean wasCanceled;
+ synchronized (lock) {
+ if (executed) {
+ throw new IllegalStateException("Already Executed");
+ }
+ executed = true;
+ this.callback = callback;
+ call = client.newCall(request);
+ currentCall = call;
+ wasCanceled = canceled;
+ }
+ if (wasCanceled) {
+ failCanceled();
+ return;
+ }
+ call.enqueue(this);
+ }
+
+ private void startRetry() {
+ Call call;
+ synchronized (lock) {
+ pendingRetry = null;
+ if (completed) {
+ return;
+ }
+ if (canceled) {
+ call = null;
+ } else {
+ call = client.newCall(request);
+ currentCall = call;
+ }
+ }
+ if (call == null) {
+ failCanceled();
+ return;
+ }
+ try {
+ call.enqueue(this);
+ } catch (RuntimeException e) {
+ // Runs on the retry scheduler, where an uncaught exception would leave the callback never invoked.
+ onFailure(call, new IOException("Failed to enqueue retry attempt", e));
+ }
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) throws IOException {
+ if (backoff != null && shouldRetry(response.code())) {
+ Optional delay = backoff.nextBackoff(response);
+ if (delay.isPresent()) {
+ Response buffered = bufferResponse(response);
+ synchronized (lock) {
+ if (!canceled && !completed) {
+ closePreviousResponse();
+ previousResponse = buffered;
+ pendingRetry = RetryScheduler.INSTANCE.schedule(
+ this::startRetry, delay.get().toMillis(), TimeUnit.MILLISECONDS);
+ return;
+ }
+ }
+ buffered.close();
+ failCanceled();
+ return;
+ }
+ }
+ boolean deliver;
+ synchronized (lock) {
+ deliver = !canceled && !completed;
+ if (deliver) {
+ completed = true;
+ closePreviousResponse();
+ }
+ }
+ if (!deliver) {
+ response.close();
+ failCanceled();
+ return;
+ }
+ callback.onResponse(call, response);
+ }
+
+ @Override
+ public void onFailure(Call call, IOException e) {
+ Response previous;
+ synchronized (lock) {
+ if (completed) {
+ return;
+ }
+ completed = true;
+ // As in the synchronous retry loop, a cancelled attempt (cancel() or the call timeout firing)
+ // reports its own failure rather than the response received before it.
+ previous = canceled || call.isCanceled() ? null : previousResponse;
+ previousResponse = null;
+ closePreviousResponse();
+ }
+ if (previous == null) {
+ callback.onFailure(call, e);
+ return;
+ }
+ // A retry attempt failed with a transport error; the response already received from the API is more
+ // actionable, as in the synchronous retry loop. As with OkHttp, the callback owns the response once it is
+ // delivered, and is not called again if onResponse throws.
+ try {
+ callback.onResponse(call, previous);
+ } catch (IOException | RuntimeException callbackError) {
+ previous.close();
+ if (callbackError instanceof RuntimeException) {
+ throw (RuntimeException) callbackError;
+ }
+ }
+ }
+
+ @Override
+ public void cancel() {
+ Call call;
+ boolean wasWaiting;
+ synchronized (lock) {
+ if (canceled) {
+ return;
+ }
+ canceled = true;
+ wasWaiting = pendingRetry != null && pendingRetry.cancel(false);
+ pendingRetry = null;
+ closePreviousResponse();
+ call = currentCall;
+ }
+ if (call != null) {
+ call.cancel();
+ }
+ if (wasWaiting) {
+ failCanceled();
+ }
+ }
+
+ /** Completes the call with a cancellation failure, unless the callback has already been invoked. */
+ private void failCanceled() {
+ Call call;
+ synchronized (lock) {
+ if (completed) {
+ return;
+ }
+ completed = true;
+ closePreviousResponse();
+ call = currentCall;
+ }
+ callback.onFailure(call, new IOException("Canceled"));
+ }
+
+ private void closePreviousResponse() {
+ if (previousResponse != null) {
+ previousResponse.close();
+ previousResponse = null;
+ }
+ }
+ }
+
/**
* Pauses the OkHttp call timeout so that it applies to each attempt individually rather than to the whole
* retry loop: the backoff wait (e.g. a {@code Retry-After} the size of the call timeout) does not consume the
diff --git a/src/main/java/com/newscatcher/catchall/core/SseEventParser.java b/src/main/java/com/newscatcher/catchall/core/SseEventParser.java
index 4118d9d..641fc8c 100644
--- a/src/main/java/com/newscatcher/catchall/core/SseEventParser.java
+++ b/src/main/java/com/newscatcher/catchall/core/SseEventParser.java
@@ -51,6 +51,27 @@ private SseEventParser() {
*/
public static T parseEventLevelUnion(
String eventType, String data, String id, Long retry, Class unionClass, String discriminatorProperty) {
+ return parseEventLevelUnion(eventType, data, id, retry, unionClass, discriminatorProperty, null);
+ }
+
+ /**
+ * Parse an SSE event using event-level discrimination.
+ *
+ * Events in {@code envelopeEvents} (or every event, if {@code envelopeEvents} is null) are deserialized from the
+ * SSE envelope ({@code event}, {@code data}, {@code id}, {@code retry}). Other events are deserialized from the
+ * JSON {@code data} payload with the discriminator injected.
+ */
+ public static T parseEventLevelUnion(
+ String eventType,
+ String data,
+ String id,
+ Long retry,
+ Class unionClass,
+ String discriminatorProperty,
+ Set envelopeEvents) {
+ if (envelopeEvents != null && eventType != null && !envelopeEvents.contains(eventType)) {
+ return parsePayloadWithInjectedDiscriminator(eventType, data, unionClass, discriminatorProperty);
+ }
try {
// Determine if data should be parsed as JSON based on the variant's expected type
Object parsedData = parseDataForVariant(eventType, data, unionClass, discriminatorProperty);
@@ -74,6 +95,21 @@ public static T parseEventLevelUnion(
}
}
+ private static T parsePayloadWithInjectedDiscriminator(
+ String eventType, String data, Class unionClass, String discriminatorProperty) {
+ try {
+ Map payload = new HashMap<>();
+ if (data != null && !data.isEmpty()) {
+ payload.putAll(ObjectMappers.JSON_MAPPER.readValue(data, new TypeReference