Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@ src/main/java/com/deepgram/core/ReconnectingWebSocketListener.java
# server control frames look fatal to deployed clients. Patched to a no-op (the raw
# frame is still delivered via onMessage(String)); mirrors the JS/Python SDKs. Re-apply
# after regen. Unfreeze once the generator stops treating unknown frames as errors.
# NOTE: the listen v2 client below also carries the multi-value query param fix documented lower
# down (keep it frozen until BOTH that fix and this one are upstreamed).
# NOTE: both the speak v2 and listen v2 clients below also carry the multi-value query param fix
# documented lower down (keep them frozen until BOTH that fix and this one are upstreamed).
src/main/java/com/deepgram/resources/speak/v2/websocket/V2WebSocketClient.java
src/main/java/com/deepgram/resources/listen/v2/websocket/V2WebSocketClient.java

# Multi-value query param serialization fix: the generated streaming clients serialize every
# array-valued query param (keyterm, keywords, replace, search, tag, extra) with
# String.valueOf(union.get()), which stringifies a List<String> into a single param
# (keyterm=[a, b]) instead of repeated params (keyterm=a&keyterm=b) — the server treats the
# array-valued query param (listen: keyterm, keywords, replace, search, tag, extra, language_hint;
# speak: tag) with String.valueOf(union.get()), which stringifies a List<String> into a single
# param (keyterm=[a, b]) instead of repeated params (keyterm=a&keyterm=b) — the server treats the
# former as one nonsense term. Patched to route these through QueryStringMapper(arraysAsRepeats=
# true), matching the REST path. The v2 listen client is already frozen above; freeze the v1
# listen client here too so the fix survives regen. Re-apply after regen; unfreeze once the
# true), matching the REST path. The v2 listen and speak clients are already frozen above; freeze
# the v1 listen client here too so the fix survives regen. Re-apply after regen; unfreeze once the
# generator serializes streaming array query params as repeats (tracked as an upstream Fern request).
src/main/java/com/deepgram/resources/listen/v1/websocket/V1WebSocketClient.java

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.deepgram.core.ClientOptions;
import com.deepgram.core.DisconnectReason;
import com.deepgram.core.ObjectMappers;
import com.deepgram.core.QueryStringMapper;
import com.deepgram.core.ReconnectingWebSocketListener;
import com.deepgram.core.RequestOptions;
import com.deepgram.core.WebSocketReadyState;
Expand Down Expand Up @@ -124,7 +125,12 @@ public CompletableFuture<Void> connect(V2ConnectOptions options) {
"mip_opt_out", String.valueOf(options.getMipOptOut().get()));
}
if (options.getTag() != null && options.getTag().isPresent()) {
urlBuilder.addQueryParameter("tag", String.valueOf(options.getTag().get()));
// Array-valued query params (String | List<String> unions) must serialize as repeated
// params (tag=a&tag=b), not a stringified list. The generated streaming template uses
// String.valueOf(...), which mangles a List into "[a, b]"; route through
// QueryStringMapper (arraysAsRepeats=true) so the wire format matches the REST path.
QueryStringMapper.addQueryParameter(
urlBuilder, "tag", options.getTag().get().get(), true);
}
Request.Builder requestBuilder = new Request.Builder().url(urlBuilder.build());
clientOptions.headers((RequestOptions) null).forEach(requestBuilder::addHeader);
Expand Down
89 changes: 89 additions & 0 deletions src/test/java/com/deepgram/SpeakV2ConnectWireTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.deepgram;

import static org.assertj.core.api.Assertions.assertThat;

import com.deepgram.core.Environment;
import com.deepgram.resources.speak.v2.websocket.V2ConnectOptions;
import com.deepgram.types.SpeakV2Tag;
import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.HttpUrl;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
* Hand-written connect-handshake wire coverage for the array-valued {@code tag} query param on
* {@code speak().v2().v2WebSocket().connect(...)} (GET /v2/speak upgrade).
*
* <p>Guards that a multi-value {@code tag} ({@code String | List<String>} union) serializes as
* repeated params ({@code tag=a&tag=b}) rather than a single stringified list ({@code tag=[a, b]}).
* Mirrors the listen v1/v2 connect wire tests. Frozen via {@code src/test/} in .fernignore.
*/
class SpeakV2ConnectWireTest {
private MockWebServer server;
private DeepgramClient client;

@BeforeEach
void setUp() throws Exception {
server = new MockWebServer();
server.start();
String base = server.url("/").toString().replaceAll("/$", "");
Environment env = Environment.custom()
.base(base)
.production(base)
.agent(base)
.agentRest(base)
.build();
client = DeepgramClient.builder().apiKey("test").environment(env).build();
}

@AfterEach
void tearDown() throws Exception {
server.shutdown();
}

private HttpUrl connectAndCaptureUrl(V2ConnectOptions options) throws Exception {
server.enqueue(new MockResponse().withWebSocketUpgrade(new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, okhttp3.Response response) {
webSocket.close(1000, null);
}
}));
client.speak().v2().v2WebSocket().connect(options);
RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS);
assertThat(request).as("handshake request was sent").isNotNull();
HttpUrl url = HttpUrl.parse(server.url("/").scheme() + "://" + request.getHeader("Host") + request.getPath());
assertThat(url).as("parsed handshake URL").isNotNull();
assertThat(url.encodedPath()).isEqualTo("/v2/speak");
return url;
}

@Test
@DisplayName("multiple tags are sent as repeated params, not a stringified list")
void tagListSentAsRepeatedParams() throws Exception {
HttpUrl url = connectAndCaptureUrl(V2ConnectOptions.builder()
.model("aura-2-thalia-en")
.tag(SpeakV2Tag.of(List.of("a", "b")))
.build());

assertThat(url.queryParameterValues("tag")).containsExactly("a", "b");
}

@Test
@DisplayName("a single string tag is sent as one param")
void tagStringSentAsOneParam() throws Exception {
HttpUrl url = connectAndCaptureUrl(V2ConnectOptions.builder()
.model("aura-2-thalia-en")
.tag(SpeakV2Tag.of("a"))
.build());

assertThat(url.queryParameterValues("tag")).containsExactly("a");
}
}
Loading