mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-13 02:18:16 +01:00
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:
@@ -32,16 +32,19 @@ public class SettingsController {
|
||||
|
||||
@AutoJobPostMapping("/update-enable-analytics")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateApiKey(@RequestParam Boolean enabled) throws IOException {
|
||||
public ResponseEntity<Map<String, Object>> updateApiKey(@RequestParam Boolean enabled)
|
||||
throws IOException {
|
||||
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
|
||||
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
|
||||
.body(
|
||||
"Setting has already been set, To adjust please edit "
|
||||
+ InstallationPathConfig.getSettingsPath());
|
||||
Map.of(
|
||||
"message",
|
||||
"Setting has already been set, To adjust please edit "
|
||||
+ InstallationPathConfig.getSettingsPath()));
|
||||
}
|
||||
GeneralUtils.saveKeyToSettings("system.enableAnalytics", enabled);
|
||||
applicationProperties.getSystem().setEnableAnalytics(enabled);
|
||||
return ResponseEntity.ok("Updated");
|
||||
return ResponseEntity.ok(Map.of("message", "Updated"));
|
||||
}
|
||||
|
||||
@GetMapping("/get-endpoints-status")
|
||||
|
||||
@@ -163,12 +163,13 @@ public class AdminSettingsController {
|
||||
responseCode = "500",
|
||||
description = "Failed to save settings to configuration file")
|
||||
})
|
||||
public ResponseEntity<String> updateSettings(
|
||||
public ResponseEntity<Map<String, Object>> updateSettings(
|
||||
@Valid @RequestBody UpdateSettingsRequest request) {
|
||||
try {
|
||||
Map<String, Object> settings = request.getSettings();
|
||||
if (settings == null || settings.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body("No settings provided to update");
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "No settings provided to update"));
|
||||
}
|
||||
|
||||
int updatedCount = 0;
|
||||
@@ -178,7 +179,11 @@ public class AdminSettingsController {
|
||||
|
||||
if (!isValidSettingKey(key)) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("Invalid setting key format: " + HtmlUtils.htmlEscape(key));
|
||||
.body(
|
||||
Map.of(
|
||||
"error",
|
||||
"Invalid setting key format: "
|
||||
+ HtmlUtils.htmlEscape(key)));
|
||||
}
|
||||
|
||||
log.info("Admin updating setting: {} = {}", key, value);
|
||||
@@ -191,22 +196,26 @@ public class AdminSettingsController {
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(
|
||||
String.format(
|
||||
"Successfully updated %d setting(s). Changes will take effect on"
|
||||
+ " application restart.",
|
||||
updatedCount));
|
||||
Map.of(
|
||||
"message",
|
||||
String.format(
|
||||
"Successfully updated %d setting(s). Changes will take effect on"
|
||||
+ " application restart.",
|
||||
updatedCount)));
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to save settings to file: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(GENERIC_FILE_ERROR);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("error", GENERIC_FILE_ERROR));
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Invalid setting key or value: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(GENERIC_INVALID_SETTING);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Map.of("error", GENERIC_INVALID_SETTING));
|
||||
} catch (Exception e) {
|
||||
log.error("Unexpected error while updating settings: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(GENERIC_SERVER_ERROR);
|
||||
.body(Map.of("error", GENERIC_SERVER_ERROR));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,20 +292,23 @@ public class AdminSettingsController {
|
||||
description = "Access denied - Admin role required"),
|
||||
@ApiResponse(responseCode = "500", description = "Failed to save settings")
|
||||
})
|
||||
public ResponseEntity<String> updateSettingsSection(
|
||||
public ResponseEntity<Map<String, Object>> updateSettingsSection(
|
||||
@PathVariable String sectionName, @Valid @RequestBody Map<String, Object> sectionData) {
|
||||
try {
|
||||
if (sectionData == null || sectionData.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body("No section data provided to update");
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "No section data provided to update"));
|
||||
}
|
||||
|
||||
if (!isValidSectionName(sectionName)) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(
|
||||
"Invalid section name: "
|
||||
+ HtmlUtils.htmlEscape(sectionName)
|
||||
+ ". Valid sections: "
|
||||
+ String.join(", ", VALID_SECTION_NAMES));
|
||||
Map.of(
|
||||
"error",
|
||||
"Invalid section name: "
|
||||
+ HtmlUtils.htmlEscape(sectionName)
|
||||
+ ". Valid sections: "
|
||||
+ String.join(", ", VALID_SECTION_NAMES)));
|
||||
}
|
||||
|
||||
// Auto-enable premium features if license key is provided
|
||||
@@ -317,7 +329,11 @@ public class AdminSettingsController {
|
||||
|
||||
if (!isValidSettingKey(fullKey)) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("Invalid setting key format: " + HtmlUtils.htmlEscape(fullKey));
|
||||
.body(
|
||||
Map.of(
|
||||
"error",
|
||||
"Invalid setting key format: "
|
||||
+ HtmlUtils.htmlEscape(fullKey)));
|
||||
}
|
||||
|
||||
log.info("Admin updating section setting: {} = {}", fullKey, value);
|
||||
@@ -331,21 +347,25 @@ public class AdminSettingsController {
|
||||
|
||||
String escapedSectionName = HtmlUtils.htmlEscape(sectionName);
|
||||
return ResponseEntity.ok(
|
||||
String.format(
|
||||
"Successfully updated %d setting(s) in section '%s'. Changes will take"
|
||||
+ " effect on application restart.",
|
||||
updatedCount, escapedSectionName));
|
||||
Map.of(
|
||||
"message",
|
||||
String.format(
|
||||
"Successfully updated %d setting(s) in section '%s'. Changes will take"
|
||||
+ " effect on application restart.",
|
||||
updatedCount, escapedSectionName)));
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to save section settings to file: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(GENERIC_FILE_ERROR);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("error", GENERIC_FILE_ERROR));
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Invalid section data: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(GENERIC_INVALID_SECTION);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Map.of("error", GENERIC_INVALID_SECTION));
|
||||
} catch (Exception e) {
|
||||
log.error("Unexpected error while updating section settings: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(GENERIC_SERVER_ERROR);
|
||||
.body(Map.of("error", GENERIC_SERVER_ERROR));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,7 +473,7 @@ public class AdminSettingsController {
|
||||
description = "Access denied - Admin role required"),
|
||||
@ApiResponse(responseCode = "500", description = "Failed to initiate restart")
|
||||
})
|
||||
public ResponseEntity<String> restartApplication() {
|
||||
public ResponseEntity<Map<String, Object>> restartApplication() {
|
||||
try {
|
||||
log.warn("Admin initiated application restart");
|
||||
|
||||
@@ -465,13 +485,18 @@ public class AdminSettingsController {
|
||||
log.error("Cannot restart: not running from JAR (likely development mode)");
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(
|
||||
"Restart not available in development mode. Please restart the application manually.");
|
||||
Map.of(
|
||||
"error",
|
||||
"Restart not available in development mode. Please restart the application manually."));
|
||||
}
|
||||
|
||||
if (helperJar == null || !Files.isRegularFile(helperJar)) {
|
||||
log.error("Cannot restart: restart-helper.jar not found at expected location");
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body("Restart helper not found. Please restart the application manually.");
|
||||
.body(
|
||||
Map.of(
|
||||
"error",
|
||||
"Restart helper not found. Cannot perform application restart."));
|
||||
}
|
||||
|
||||
// Get current application arguments
|
||||
@@ -526,12 +551,17 @@ public class AdminSettingsController {
|
||||
.start();
|
||||
|
||||
return ResponseEntity.ok(
|
||||
"Application restart initiated. The server will be back online shortly.");
|
||||
Map.of(
|
||||
"message",
|
||||
"Application restart initiated. The server will be back online shortly."));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to initiate restart: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Failed to initiate application restart: " + e.getMessage());
|
||||
.body(
|
||||
Map.of(
|
||||
"error",
|
||||
"Failed to initiate application restart: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -741,31 +741,35 @@ public class UserController {
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/get-api-key")
|
||||
public ResponseEntity<String> getApiKey(Principal principal) {
|
||||
public ResponseEntity<Map<String, String>> getApiKey(Principal principal) {
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(Map.of("error", "User not authenticated."));
|
||||
}
|
||||
String username = principal.getName();
|
||||
String apiKey = userService.getApiKeyForUser(username);
|
||||
if (apiKey == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(Map.of("error", "API key not found for user."));
|
||||
}
|
||||
return ResponseEntity.ok(apiKey);
|
||||
return ResponseEntity.ok(Map.of("apiKey", apiKey));
|
||||
}
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/update-api-key")
|
||||
public ResponseEntity<String> updateApiKey(Principal principal) {
|
||||
public ResponseEntity<Map<String, String>> updateApiKey(Principal principal) {
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(Map.of("error", "User not authenticated."));
|
||||
}
|
||||
String username = principal.getName();
|
||||
User user = userService.refreshApiKeyForUser(username);
|
||||
String apiKey = user.getApiKey();
|
||||
if (apiKey == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(Map.of("error", "API key not found for user."));
|
||||
}
|
||||
return ResponseEntity.ok(apiKey);
|
||||
return ResponseEntity.ok(Map.of("apiKey", apiKey));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user