{"record":{"id":"db0dca1e5303eec0","repo":"gethomepage/homepage","slug":"failed-parsing-action-response","errorCode":null,"errorMessage":"Failed parsing '${action}' response","messagePattern":"Failed parsing '(.+?)' response","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/widgets/fritzbox/proxy.js","lineNumber":42,"sourceCode":"      \"</s:Body>\" +\n      \"</s:Envelope>\",\n  };\n  const apiUrl = `${apiBaseUrl}/igdupnp/control/${servicePath}`;\n  const [status, , data] = await httpProxy(apiUrl, params);\n  if (status !== 200) {\n    logger.debug(`HTTP ${status} performing SoapRequest for ${service}->${action}`, data);\n    throw new Error(`Failed fetching '${action}'`);\n  }\n  const response = {};\n  try {\n    const jsonData = JSON.parse(xml2json(data));\n    const responseElements = jsonData?.elements?.[0]?.elements?.[0]?.elements?.[0]?.elements || [];\n    responseElements.forEach((element) => {\n      response[element.name] = element.elements?.[0].text || \"\";\n    });\n  } catch (e) {\n    logger.debug(`Failed parsing ${service}->${action} response:`, data);\n    throw new Error(`Failed parsing '${action}' response`);\n  }\n\n  return response;\n}\n\nexport default async function fritzboxProxyHandler(req, res) {\n  const { group, service, index } = req.query;\n  const serviceWidget = await getServiceWidget(group, service, index);\n\n  if (!serviceWidget) {\n    res.status(500).json({ error: { message: \"Service widget not found\" } });\n    return;\n  }\n\n  if (!serviceWidget.url) {\n    res.status(500).json({ error: { message: \"Service widget url not configured\" } });\n    return;\n  }","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/gethomepage/homepage/blob/b6dca1ae033e613d8e692f9a161a3cc53a5a2857/src/widgets/fritzbox/proxy.js#L24-L60","documentation":"Thrown when httpProxy returned 200 for a Fritz!Box SOAP action but the response body could not be parsed: either JSON.parse(xml2json(data)) threw, or the deeply-nested element path (elements[0].elements[0].elements[0].elements) was not the expected shape. The router answered, but not in the SOAP envelope form the parser assumes.","triggerScenarios":"The router returned a SOAP fault envelope (still 200) whose structure differs from a normal response, an HTML login/error page with a 200 status, a truncated response from a flaky proxy, or a firmware change that alters the envelope nesting.","commonSituations":"Fritz!Box returning a fault/permission-denied page with HTTP 200; an intermediary reverse proxy rewriting the body; partial response due to connection drop; a service whose response legitimately omits one nesting level (so the path resolves to undefined).","solutions":["Inspect the debug log line 'Failed parsing ${service}->${action} response:' which dumps the raw data just before the throw.","Confirm the action/service actually permits this call for the current Fritz!Box user; an unauthorized SOAP call often returns a fault envelope rather than a real payload.","If a firmware update changed the envelope, adjust the element path parsing or upgrade Homepage to a version that matches the firmware.","Remove any reverse proxy in front of the Fritz!Box that may transform/truncate the SOAP body.","Verify the request is hitting /igdupnp/control/... and not a redirect that returns HTML."],"exampleFix":"// before\nconst jsonData = JSON.parse(xml2json(data));\n// after\nlet jsonData;\ntry {\n  jsonData = JSON.parse(xml2json(data));\n} catch (e) {\n  throw new Error(`Failed to convert Fritz!Box response to JSON for ${action}: ${e.message}; raw=${Buffer.from(data).toString().slice(0, 200)}`);\n}","handlingStrategy":"try-catch","validationCode":"// Detect a SOAP fault early so it is not misread as a parse failure.\nfunction looksLikeSoapFault(raw) {\n  const s = Buffer.from(raw).toString();\n  return /<fault>|<faultcode>|UPnPError/i.test(s);\n}","typeGuard":"function isFritzboxSoapResponse(json) {\n  const node = json?.elements?.[0]?.elements?.[0]?.elements?.[0]?.elements;\n  return Array.isArray(node) && node.length > 0;\n}","tryCatchPattern":"try {\n  const jsonData = JSON.parse(xml2json(data));\n  if (!isFritzboxSoapResponse(jsonData)) {\n    throw new Error(`Unexpected Fritz!Box SOAP envelope shape for ${action}`);\n  }\n  // ...build response\n} catch (e) {\n  if (looksLikeSoapFault(data)) {\n    res.status(403).json({ error: `Fritz!Box denied ${action} (SOAP fault)` });\n  } else {\n    res.status(502).json({ error: `Could not parse Fritz!Box ${action} response` });\n  }\n}","preventionTips":["Log the raw body once at debug level whenever parsing fails so the cause is visible.","Distinguish SOAP faults from malformed envelopes; they need different fixes (permissions vs firmware).","Pin Homepage and Fritz!OS versions together in staging to catch envelope-shape regressions."],"tags":["fritzbox","soap","parsing","xml","upnp","widget"],"backgroundTag":null,"analyzedSha":"b6dca1ae033e613d8e692f9a161a3cc53a5a2857","analyzedAt":"2026-08-13T04:48:44.121Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}