mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 06:11:45 +00:00
fix(ntfy): encode non-Latin-1 header values with RFC 2047 to prevent ByteString crash
Todo/trip names containing chars like → or € (and non-Latin-1 locale templates for Czech, Chinese, Russian, etc.) caused the Fetch API to throw when setting the ntfy Title header. Apply RFC 2047 base64 encoded-word encoding for any header value containing chars above U+00FF; ntfy decodes this automatically.
This commit is contained in:
@@ -621,6 +621,15 @@ export function isNtfyConfiguredAdmin(): boolean {
|
||||
return !!(getAppSetting('admin_ntfy_topic'));
|
||||
}
|
||||
|
||||
function encodeHeaderValue(value: string): string {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (value.charCodeAt(i) > 0xFF) {
|
||||
return `=?UTF-8?B?${Buffer.from(value, 'utf8').toString('base64')}?=`;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function sendNtfy(
|
||||
url: string,
|
||||
token: string | null,
|
||||
@@ -638,11 +647,11 @@ export async function sendNtfy(
|
||||
|
||||
// ntfy header-based API: POST to topic URL, body = plain text message, metadata in headers
|
||||
const headers: Record<string, string> = {
|
||||
'Title': payload.title,
|
||||
'Title': encodeHeaderValue(payload.title),
|
||||
'Priority': String(meta.priority),
|
||||
};
|
||||
if (meta.tags.length > 0) headers['Tags'] = meta.tags.join(',');
|
||||
if (payload.link) headers['Click'] = payload.link;
|
||||
if (payload.link) headers['Click'] = encodeHeaderValue(payload.link);
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
try {
|
||||
|
||||
@@ -455,4 +455,27 @@ describe('sendNtfy', () => {
|
||||
expect(calledOpts.headers['Priority']).toBe('3');
|
||||
expect(calledOpts.headers['Tags']).toBeUndefined(); // empty tags = no header
|
||||
});
|
||||
|
||||
it('NTFY-009 — title with non-Latin-1 chars is RFC 2047 encoded', async () => {
|
||||
const mockFetch = globalThis.fetch as unknown as ReturnType<typeof vi.fn>;
|
||||
mockFetch.mockResolvedValueOnce({ ok: true, text: async () => '' } as never);
|
||||
|
||||
await sendNtfy(ntfyUrl, null, { ...payload, title: 'Buy →€ ticket' });
|
||||
|
||||
const [, calledOpts] = mockFetch.mock.calls[0];
|
||||
const encoded = calledOpts.headers['Title'] as string;
|
||||
expect(encoded).toMatch(/^=\?UTF-8\?B\?/);
|
||||
const b64 = encoded.replace(/^=\?UTF-8\?B\?/, '').replace(/\?=$/, '');
|
||||
expect(Buffer.from(b64, 'base64').toString('utf8')).toBe('Buy →€ ticket');
|
||||
});
|
||||
|
||||
it('NTFY-010 — ASCII-only title is passed through verbatim', async () => {
|
||||
const mockFetch = globalThis.fetch as unknown as ReturnType<typeof vi.fn>;
|
||||
mockFetch.mockResolvedValueOnce({ ok: true, text: async () => '' } as never);
|
||||
|
||||
await sendNtfy(ntfyUrl, null, { ...payload, title: 'Simple ASCII title' });
|
||||
|
||||
const [, calledOpts] = mockFetch.mock.calls[0];
|
||||
expect(calledOpts.headers['Title']).toBe('Simple ASCII title');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user