Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.Locale;

public final class GasTranslationProvider implements TranslationProvider {
private static final int MAX_REDIRECTS = 5;
private final HttpClient httpClient = HttpClient.newHttpClient();

@Override
Expand All @@ -38,14 +39,12 @@ public TranslationResult translate(TranslationRequest request, ChatglotConfig co
payload.addProperty("source", normalizeLanguageCode(request.sourceLanguageHint()));
}

HttpRequest httpRequest = HttpRequest.newBuilder(URI.create(config.gasWebAppUrl.trim()))
.timeout(Duration.ofSeconds(config.requestTimeoutSeconds))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload.toString(), StandardCharsets.UTF_8))
.build();

try {
HttpResponse<String> response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
HttpResponse<String> response = sendWithRedirects(
config.gasWebAppUrl.trim(),
payload.toString(),
config.requestTimeoutSeconds
);
if (response.statusCode() >= 400) {
throw new TranslationException(
"GAS translation request failed ("
Expand All @@ -55,7 +54,24 @@ public TranslationResult translate(TranslationRequest request, ChatglotConfig co
);
}

JsonObject root = JsonParser.parseString(response.body()).getAsJsonObject();
String responseBody = response.body() == null ? "" : response.body();
String normalizedBody = normalizeJsonBody(responseBody);

JsonObject root;
try {
root = JsonParser.parseString(normalizedBody).getAsJsonObject();
} catch (RuntimeException parseError) {
throw new TranslationException(
"GAS returned non-JSON response ("
+ response.statusCode()
+ ", "
+ readContentType(response)
+ "): "
+ TranslationPromptBuilder.abbreviate(responseBody, 500),
parseError
);
}

if (root.has("ok") && !root.get("ok").getAsBoolean()) {
throw new TranslationException("GAS translation failed: " + extractErrorMessage(root));
}
Expand All @@ -78,6 +94,70 @@ public TranslationResult translate(TranslationRequest request, ChatglotConfig co
}
}

private HttpResponse<String> sendWithRedirects(String url, String requestBody, int timeoutSeconds) throws Exception {
URI currentUri = URI.create(url);
boolean usePost = true;

for (int attempt = 0; attempt <= MAX_REDIRECTS; attempt++) {
HttpRequest.Builder builder = HttpRequest.newBuilder(currentUri)
.timeout(Duration.ofSeconds(timeoutSeconds));
HttpRequest request;
if (usePost) {
request = builder
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
.build();
} else {
request = builder.GET().build();
}

HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
int status = response.statusCode();
if (status < 300 || status >= 400) {
return response;
}

String location = response.headers().firstValue("Location").orElse("");
if (location.isBlank()) {
return response;
}

// GAS /exec commonly redirects POST (302) to a googleusercontent URL that should be fetched with GET.
if (status == 301 || status == 302 || status == 303) {
usePost = false;
}
currentUri = currentUri.resolve(location.trim());
}

throw new TranslationException("GAS translation request failed: too many redirects.");
}

private static String normalizeJsonBody(String body) {
if (body == null) {
return "";
}

String normalized = body.stripLeading();
if (normalized.startsWith("\uFEFF")) {
normalized = normalized.substring(1).stripLeading();
}

if (normalized.startsWith(")]}'")) {
int lineBreak = normalized.indexOf('\n');
if (lineBreak >= 0) {
normalized = normalized.substring(lineBreak + 1).stripLeading();
} else {
normalized = normalized.substring(4).stripLeading();
}
}

return normalized;
}

private static String readContentType(HttpResponse<String> response) {
return response.headers().firstValue("Content-Type").orElse("unknown");
}

private static String extractErrorMessage(JsonObject root) {
String message = getString(root, "message");
String details = getString(root, "details");
Expand Down