actualbudget/actual · warning

Method not allowed

Error message

Method not allowed

What it means

After type-checking, the proxy normalizes the method to uppercase and only permits GET and HEAD for proxied requests. Any other verb produces 405 'Method not allowed'. This restricts the proxy to read-only operations by design.

Source

Thrown at packages/sync-server/src/app-cors-proxy.js:177

  // Check if the URL is allowed
  if (!isUrlAllowed(url.href)) {
    console.warn('Blocked request to unauthorized URL:', url.href);
    return res.status(403).json({
      error: 'URL not allowed',
      message:
        'Only allowlisted plugin repositories are allowed (localhost only in development)',
    });
  }

  try {
    const { method = 'GET', headers: customHeaders = {} } = req.body || {};

    if (typeof method !== 'string') {
      return res.status(400).json({ error: 'Invalid method parameter' });
    }
    const methodNormalized = method.toUpperCase();
    if (!['GET', 'HEAD'].includes(methodNormalized)) {
      return res.status(405).json({ error: 'Method not allowed' });
    }

    const requestHeaders = {
      ...req.headers,
      ...customHeaders,
      host: url.host,
    };

    // Remove headers that shouldn't be forwarded
    delete requestHeaders['x-actual-token'];
    delete requestHeaders['content-length'];
    delete requestHeaders['cookie'];
    delete requestHeaders['cookie2'];

    // Add GitHub authentication if token is configured and request is to GitHub
    const githubToken = config.get('github.token');
    if (
      githubToken &&

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Use GET (or HEAD) for the proxied request — the proxy is intentionally read-only.
  2. If a write operation is needed, use a dedicated API/integration path instead of the CORS proxy.
  3. Ensure you send the verb in the JSON body's `method` field, not just via the HTTP method of the proxy request itself.

Example fix

// before
proxy({ url: target, method: 'POST', body: payload });
// after
proxy({ url: target, method: 'GET' }); // read-only proxy
Defensive patterns

Strategy: validation

Validate before calling

const method = (opts.method ?? 'GET').toUpperCase();
if (!['GET', 'HEAD'].includes(method)) {
  throw new Error(`Proxy supports only GET/HEAD, got ${method}`);
}

Type guard

function isProxyMethod(m) {
  return typeof m === 'string' && ['GET', 'HEAD'].includes(m.toUpperCase());
}

Try / catch

try {
  return await proxy({ url, method });
} catch (e) {
  if (e.status === 405) {
    console.error('CORS proxy is read-only: use GET or HEAD');
  }
  throw e;
}

Prevention

When it happens

Trigger: Requesting the proxy with body { method: 'POST' }, 'PUT', 'DELETE', 'PATCH', etc., or a lowercase variant that normalizes to a disallowed verb.

Common situations: A plugin trying to publish/upload through the proxy; reusing client code that defaults to POST; assuming the proxy forwards the incoming request's own method instead of reading the body field.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/90f3ddd3a226a25b. Report an issue: GitHub.