{"record":{"id":"27c7652a067d4d88","repo":"SubtitleEdit/subtitleedit","slug":"api-key-invalid-or-perhaps-api-billing-is-not-ena","errorCode":null,"errorMessage":"API key invalid (or perhaps API/billing is not enabled)?","messagePattern":"API key invalid \\(or perhaps API/billing is not enabled\\)\\?","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/libuilogic/AutoTranslate/GoogleTranslateV2.cs","lineNumber":102,"sourceCode":"                {\n                    throw new Exception($\"An error occurred calling GT translate - status code: {result.StatusCode}\");\n                }\n\n                content = await result.Content.ReadAsStringAsync(cancellationToken);\n            }\n            catch (WebException webException)\n            {\n                var message = string.Empty;\n                if (webException.Message.Contains(\"(400) Bad Request\"))\n                {\n                    message = \"API key invalid (or perhaps API/billing is not enabled)?\";\n                }\n                else if (webException.Message.Contains(\"(403) Forbidden.\"))\n                {\n                    message = \"Perhaps billing is not enabled (or API not enabled or API key is invalid)?\";\n                }\n\n                throw new Exception(message, webException);\n            }\n\n            var resultList = new List<string>();\n            var parser = new JsonParser();\n            var x = (Dictionary<string, object>)parser.Parse(content);\n            foreach (var k in x.Keys)\n            {\n                if (x[k] is Dictionary<string, object> v)\n                {\n                    foreach (var innerKey in v.Keys)\n                    {\n                        if (v[innerKey] is List<object> l)\n                        {\n                            foreach (var o2 in l)\n                            {\n                                if (o2 is Dictionary<string, object> v2)\n                                {\n                                    foreach (var innerKey2 in v2.Keys)","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/SubtitleEdit/subtitleedit/blob/17a9f0748781032255db3526b7215d2fb891e3af/src/libuilogic/AutoTranslate/GoogleTranslateV2.cs#L84-L120","documentation":"Thrown inside GoogleTranslateV2's catch (WebException) block when the exception message contains '(400) Bad Request'. This path is hit when the underlying transport (WebRequest-era stack) wraps a 400 response as a WebException rather than surfacing it as an HttpResponseMessage. The message maps 400 to an API-key/billing diagnosis. Note: if the WebException message matches neither branch, 'message' stays empty and the throw loses text (though the inner exception is preserved).","triggerScenarios":"A WebException escapes the try at GoogleTranslateV2.cs:56; its Message contains '(400) Bad Request'. Same root cause as error 47 (invalid key / billing / API not enabled) but reached via the legacy exception path.","commonSituations":"Running on a runtime/config where the 400 is thrown as WebException instead of being delivered as a response; empty key; key for a project without the Translation API or billing enabled.","solutions":["Same as error 47: verify the API key, enable the Translation API, and enable billing.","Inspect the inner WebException Response for Google's JSON error detail.","Set GoogleApiV2Key before calling Initialize/Translate."],"exampleFix":"// before\nif (webException.Message.Contains(\"(400) Bad Request\"))\n{\n    message = \"API key invalid (or perhaps API/billing is not enabled)?\";\n}\n...\nthrow new Exception(message, webException);\n\n// after - guarantee a non-empty message even when neither branch matches\nvar message = $\"GoogleTranslateV2 transport error: {webException.Status}\";\nif (webException.Message.Contains(\"(400) Bad Request\"))\n{\n    message = \"API key invalid (or perhaps API/billing is not enabled)?\";\n}\nelse if (webException.Message.Contains(\"(403) Forbidden.\"))\n{\n    message = \"Perhaps billing is not enabled (or API not enabled or API key is invalid)?\";\n}\nthrow new Exception(message, webException);","handlingStrategy":"try-catch","validationCode":"// Validate key presence before calling (the 400 transport path is just another symptom of a bad key)\nvar key = Configuration.Settings.Tools.GoogleApiV2Key;\nif (string.IsNullOrWhiteSpace(key))\n    throw new InvalidOperationException(\"Google V2 API key is not set - a 400 will follow.\");","typeGuard":null,"tryCatchPattern":"try\n{\n    return await googleV2.Translate(text, src, tgt, token);\n}\ncatch (Exception ex) when (ex.InnerException is WebException we && we.Message.Contains(\"(400) Bad Request\"))\n{\n    throw new InvalidOperationException(\"Google rejected the request (400) - invalid key or API/billing not enabled.\", ex);\n}\ncatch (Exception ex) when (string.IsNullOrWhiteSpace(ex.Message) && ex.InnerException is WebException)\n{\n    // Guard against the empty-message pitfall: always surface something\n    throw new InvalidOperationException($\"GoogleTranslateV2 transport error: {((WebException)ex.InnerException).Status}\", ex);\n}","preventionTips":["Validate the key is set and looks well-formed before the first call.","When catching WebException, always inspect Status and Response rather than string-matching the message.","Ensure a non-empty message is thrown even when neither string branch matches (current code can throw an empty message).","Enable the Translation API + billing so 400s do not occur."],"tags":["api","auth","billing","translation","google","transport"],"backgroundTag":null,"analyzedSha":"17a9f0748781032255db3526b7215d2fb891e3af","analyzedAt":"2026-08-13T18:11:43.374Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}