Completes body/header parsing of custom api widget (#2191)

This commit is contained in:
Alicia Sykes
2026-06-11 13:51:15 +01:00
parent d631e049e2
commit fc95e8ef2d
3 changed files with 29 additions and 3 deletions

View File

@@ -3310,7 +3310,7 @@ This was very much inspired by, and is broadly compatible with [Homepage's custo
**`password`** | `string` | _Optional_ | Password for HTTP basic auth
**`method`** | `string` | _Optional_ | HTTP method, e.g. `GET` (default) or `POST`
**`headers`** | `object` | _Optional_ | An object of custom request headers
**`requestBody`** | `string` or `object` | _Optional_ | Request body for non-GET methods. Prefer an object (sent as JSON) — a raw string is JSON-encoded again, so don't pre-stringify
**`requestBody`** | `string` or `object` | _Optional_ | Request body for non-GET methods. Prefer an object. A raw string is JSON-encoded again, so don't pre-stringify
**`display`** | `string` | _Optional_ | Layout for the fields: `block` (default, label and value on one row) or `list` (value stacked under the label)
**`mappings`** | `array` | _Optional_ | The fields to display from the response (see below). If omitted, the raw response root is shown
@@ -3326,7 +3326,7 @@ Each item in `mappings` accepts:
**`timeStyle`** | `string` | _Optional_ | For `date` format. One of `full`, `long`, `medium`, `short`
**`style`** | `string` | _Optional_ | For `relativeDate` format. One of `long` (default), `short`, `narrow`
**`numeric`** | `string` | _Optional_ | For `relativeDate` format. One of `always` (default) or `auto`
**`additionalField`** | `object` | _Optional_ | A secondary value shown next to the main one. Accepts `field`, `format` (and its options), plus `color` one of `theme`, `adaptive` (action colour from the value's sign: positive green, negative red), `black` or `white`
**`additionalField`** | `object` | _Optional_ | A secondary value shown next to the main one. Accepts `field`, `format` (and its options), plus `color` - one of `theme`, `adaptive` (action colour from the value's sign: positive green, negative red), `black` or `white`
Notes:
- **`percent`** treats the value as an already-computed percentage, so `42` is shown as `42%`

View File

@@ -45,7 +45,12 @@ export default {
Object.keys(userHeaders).forEach((key) => {
resolved[key] = this.parseAsEnvVar(userHeaders[key]);
});
return { ...this.authHeaders, ...resolved };
const headers = { ...this.authHeaders, ...resolved };
// Default a JSON content-type for body-bearing methods, unless the user set one
const hasBody = this.options.requestBody != null && this.method !== 'GET' && this.method !== 'HEAD';
const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === 'content-type');
if (hasBody && !hasContentType) headers['Content-Type'] = 'application/json';
return headers;
},
method() {
return (this.options.method || 'GET').toUpperCase();

View File

@@ -71,6 +71,27 @@ describe('CustomApi widget', () => {
expect(wrapper.find('.additional').classes()).toContain('color-success');
});
describe('mergedHeaders content-type', () => {
it('defaults to application/json for a body-bearing method', () => {
wrapper = mountWidget({ url: 'https://example.com', method: 'POST', requestBody: { a: 1 } });
expect(wrapper.vm.mergedHeaders['Content-Type']).toBe('application/json');
});
it('does not add a content-type for GET', () => {
wrapper = mountWidget({ url: 'https://example.com', requestBody: { a: 1 } });
const keys = Object.keys(wrapper.vm.mergedHeaders).map((k) => k.toLowerCase());
expect(keys).not.toContain('content-type');
});
it('respects a user-provided content-type', () => {
wrapper = mountWidget({
url: 'https://example.com',
method: 'POST',
requestBody: '<xml/>',
headers: { 'Content-Type': 'application/xml' },
});
expect(wrapper.vm.mergedHeaders['Content-Type']).toBe('application/xml');
});
});
describe('updateInterval', () => {
it('defaults to 10s', () => {
wrapper = mountWidget({ url: 'https://example.com' });