fix(api): return JSON responses for admin settings + API key endpoints to prevent Tauri client parse errors (#5437)

# Description of Changes

```console
index-DsORDqQQ.js:124 \n [TauriHttpClient] Network error: \n{url: 'http://localhost:8080/api/v1/admin/settings', method: 'PUT', errorType: 'ERR_NETWORK', originalMessage: `Failed to execute 'close' on 'ReadableStreamDefaul…cted token 'S', "Successful"... is not valid JSON`, stack: `SyntaxError: Unexpected token 'S', "Successful"...…lback (<anonymous>:284:7)\n    at <anonymous>:1:28`}\nerrorType\n: \n"ERR_NETWORK"\nmethod\n: \n"PUT"\noriginalMessage\n: \n"Failed to execute 'close' on 'ReadableStreamDefaultController': Unexpected token 'S', \"Successful\"... is not valid JSON"\nstack\n: \n"SyntaxError: Unexpected token 'S', \"Successful\"... is not valid JSON\n    at A.onmessage (http://tauri.localhost/assets/index-DsORDqQQ.js:124:22714)\n    at http://tauri.localhost/assets/index-DsORDqQQ.js:124:20748\n    at <anonymous>:272:26\n    at Object.runCallback (<anonymous>:284:7)\n    at <anonymous>:1:28"\nurl\n: \n"http://localhost:8080/api/v1/admin/settings"

index-DXbk7lbS.js:124 \n [TauriHttpClient] Network error: \n{url: 'http://localhost:8080/api/v1/user/get-api-key', method: 'POST', errorType: 'ERR_NETWORK', originalMessage: `Failed to execute 'close' on 'ReadableStreamDefaul…cted token 'a', "a72f6b26-1"... is not valid JSON`, stack: `SyntaxError: Unexpected token 'a', "a72f6b26-1"...…lback (<anonymous>:284:7)\n    at <anonymous>:1:28`}\nerrorType\n: \n"ERR_NETWORK"\nmethod\n: \n"POST"\noriginalMessage\n: \n"Failed to execute 'close' on 'ReadableStreamDefaultController': Unexpected token 'a', \"a72f6b26-1\"... is not valid JSON"\nstack\n: \n"SyntaxError: Unexpected token 'a', \"a72f6b26-1\"... is not valid JSON\n    at A.onmessage (http://tauri.localhost/assets/index-DXbk7lbS.js:124:22714)\n    at http://tauri.localhost/assets/index-DXbk7lbS.js:124:20748\n    at <anonymous>:272:26\n    at Object.runCallback (<anonymous>:284:7)\n    at <anonymous>:1:28"\nurl\n: \n"http://localhost:8080/api/v1/user/get-api-key"
```


This pull request fixes a self-hosting issue where the Tauri HTTP client
fails with `Unexpected token ... is not valid JSON` because certain API
endpoints returned plain text responses.

## What was changed

- Updated `AdminSettingsController`:
- Changed `updateSettings` and `updateSettingsSection` to return
structured JSON objects instead of raw strings.
- Standardized success and error payloads using a `Map<String, Object>`
with keys like `message` and `error`.
- Updated `UserController`:
- Changed `/api/v1/user/get-api-key` and `/api/v1/user/update-api-key`
to return JSON objects (`{ "apiKey": "..." }`) and JSON error objects
instead of plain text.

## Why the change was made

- The Tauri client expects JSON responses and attempts to parse them.
Returning plain strings like `"Successful..."` or an API key string
causes JSON parsing to fail, resulting in network errors on self-hosted
setups.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
Ludy
2026-01-13 22:18:09 +01:00
committed by GitHub
parent 84ed1d7ecb
commit b266b50bef
7 changed files with 582 additions and 79 deletions

View File

@@ -1,5 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import apiClient from "@app/services/apiClient";
import { alert } from "@app/components/toast";
import { useTranslation } from "react-i18next";
export function useApiKey() {
const [apiKey, setApiKey] = useState<string | null>(null);
@@ -7,49 +9,107 @@ export function useApiKey() {
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);
const [hasAttempted, setHasAttempted] = useState<boolean>(false);
const { t } = useTranslation();
function failedToCreateAlert() {
alert({
alertType: "error",
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
body: t("config.apiKeys.alert.failedToCreateApiKey", "Failed to create API key."),
isPersistentPopup: false,
});
}
const fetchKey = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
// Backend is POST for get and update
const res = await apiClient.post("/api/v1/user/get-api-key");
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
if (typeof value === "string") setApiKey(value);
} catch (e: any) {
// If not found, try to create one by calling update endpoint
if (e?.response?.status === 404) {
try {
const createRes = await apiClient.post("/api/v1/user/update-api-key");
const created =
typeof createRes.data === "string"
? createRes.data
: createRes.data?.apiKey;
if (typeof created === "string") setApiKey(created);
} catch (createErr: any) {
setError(createErr);
// Backend is POST for get and update
await apiClient
.post("/api/v1/user/get-api-key", undefined, {
responseType: "json",
})
.then((response) => {
const data = response.data;
const apiKeyValue = typeof data === "string" ? data : data?.apiKey;
if (typeof apiKeyValue === "string") {
setApiKey(apiKeyValue);
} else {
alert({
alertType: "error",
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
body: t("config.apiKeys.alert.failedToRetrieveApiKey", "Failed to retrieve API key from response."),
isPersistentPopup: false,
});
}
} else {
setError(e);
}
} finally {
setIsLoading(false);
setHasAttempted(true);
}
})
.catch(async (e) => {
// If not found, try to create one by calling update endpoint
if (e?.response?.status === 404) {
await apiClient
.post("/api/v1/user/update-api-key")
.then((createRes) => {
const created = typeof createRes.data === "string" ? createRes.data : createRes.data?.apiKey;
if (typeof created === "string") {
setApiKey(created);
} else {
failedToCreateAlert();
}
})
.catch((createErr) => {
failedToCreateAlert();
setError(createErr);
});
} else {
alert({
alertType: "error",
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
body: t("config.apiKeys.alert.failedToFetchApiKey", "Failed to fetch API key."),
isPersistentPopup: false,
});
setError(e);
}
})
.finally(() => {
setIsLoading(false);
setHasAttempted(true);
});
}, []);
const refresh = useCallback(async () => {
setIsRefreshing(true);
setError(null);
try {
const res = await apiClient.post("/api/v1/user/update-api-key");
await apiClient.post("/api/v1/user/update-api-key", undefined, {
responseType: "json",
suppressErrorToast: true,
}).then((res) => {
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
if (typeof value === "string") setApiKey(value);
} catch (e: any) {
if (typeof value === "string") {
alert({
alertType: "success",
title: t("config.apiKeys.alert.apiKeyRefreshed", "API Key Refreshed"),
body: t("config.apiKeys.alert.apiKeyRefreshedBody", "Your API key has been successfully refreshed."),
isPersistentPopup: false,
});
setApiKey(value);
} else {
alert({
alertType: "error",
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
body: t("config.apiKeys.alert.failedToRefreshApiKey", "Failed to refresh API key."),
isPersistentPopup: false,
});
}
}).catch((e) => {
alert({
alertType: "error",
title: t("config.apiKeys.alert.apiKeyErrorTitle", "API Key Error"),
body: t("config.apiKeys.alert.failedToRefreshApiKey", "Failed to refresh API key."),
isPersistentPopup: false,
});
setError(e);
} finally {
}).finally(() => {
setIsRefreshing(false);
}
});
}, []);
useEffect(() => {