{"record":{"id":"90f3ddd3a226a25b","repo":"actualbudget/actual","slug":"method-not-allowed","errorCode":null,"errorMessage":"Method not allowed","messagePattern":"Method not allowed","errorType":"http","errorClass":null,"httpStatus":405,"severity":"warning","filePath":"packages/sync-server/src/app-cors-proxy.js","lineNumber":177,"sourceCode":"  // Check if the URL is allowed\n  if (!isUrlAllowed(url.href)) {\n    console.warn('Blocked request to unauthorized URL:', url.href);\n    return res.status(403).json({\n      error: 'URL not allowed',\n      message:\n        'Only allowlisted plugin repositories are allowed (localhost only in development)',\n    });\n  }\n\n  try {\n    const { method = 'GET', headers: customHeaders = {} } = req.body || {};\n\n    if (typeof method !== 'string') {\n      return res.status(400).json({ error: 'Invalid method parameter' });\n    }\n    const methodNormalized = method.toUpperCase();\n    if (!['GET', 'HEAD'].includes(methodNormalized)) {\n      return res.status(405).json({ error: 'Method not allowed' });\n    }\n\n    const requestHeaders = {\n      ...req.headers,\n      ...customHeaders,\n      host: url.host,\n    };\n\n    // Remove headers that shouldn't be forwarded\n    delete requestHeaders['x-actual-token'];\n    delete requestHeaders['content-length'];\n    delete requestHeaders['cookie'];\n    delete requestHeaders['cookie2'];\n\n    // Add GitHub authentication if token is configured and request is to GitHub\n    const githubToken = config.get('github.token');\n    if (\n      githubToken &&","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-cors-proxy.js#L159-L195","documentation":"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.","triggerScenarios":"Requesting the proxy with body { method: 'POST' }, 'PUT', 'DELETE', 'PATCH', etc., or a lowercase variant that normalizes to a disallowed verb.","commonSituations":"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.","solutions":["Use GET (or HEAD) for the proxied request — the proxy is intentionally read-only.","If a write operation is needed, use a dedicated API/integration path instead of the CORS proxy.","Ensure you send the verb in the JSON body's `method` field, not just via the HTTP method of the proxy request itself."],"exampleFix":"// before\nproxy({ url: target, method: 'POST', body: payload });\n// after\nproxy({ url: target, method: 'GET' }); // read-only proxy","handlingStrategy":"validation","validationCode":"const method = (opts.method ?? 'GET').toUpperCase();\nif (!['GET', 'HEAD'].includes(method)) {\n  throw new Error(`Proxy supports only GET/HEAD, got ${method}`);\n}","typeGuard":"function isProxyMethod(m) {\n  return typeof m === 'string' && ['GET', 'HEAD'].includes(m.toUpperCase());\n}","tryCatchPattern":"try {\n  return await proxy({ url, method });\n} catch (e) {\n  if (e.status === 405) {\n    console.error('CORS proxy is read-only: use GET or HEAD');\n  }\n  throw e;\n}","preventionTips":["Treat the CORS proxy as read-only; route writes through proper APIs.","Normalize the method client-side before sending.","Document the GET/HEAD restriction in plugin development guides."],"tags":["cors-proxy","http-method","policy"],"backgroundTag":"method-not-allowed","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}