{"record":{"id":"ec7ee44b99ab8da9","repo":"ytdl-org/youtube-dl","slug":"s-said-s-ec7ee4","errorCode":null,"errorMessage":"%s said: %s","messagePattern":"%s said: %s","errorType":"exception","errorClass":"ExtractorError","httpStatus":null,"severity":"error","filePath":"youtube_dl/extractor/linuxacademy.py","lineNumber":126,"sourceCode":"            'sso': 'true',\n        })\n\n        login_state_url = urlh.geturl()\n\n        try:\n            login_page = self._download_webpage(\n                'https://login.linuxacademy.com/usernamepassword/login', None,\n                'Downloading login page', data=json.dumps(login_data).encode(),\n                headers={\n                    'Content-Type': 'application/json',\n                    'Origin': 'https://login.linuxacademy.com',\n                    'Referer': login_state_url,\n                })\n        except ExtractorError as e:\n            if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:\n                error = self._parse_json(e.cause.read(), None)\n                message = error.get('description') or error['code']\n                raise ExtractorError(\n                    '%s said: %s' % (self.IE_NAME, message), expected=True)\n            raise\n\n        callback_page, urlh = self._download_webpage_handle(\n            'https://login.linuxacademy.com/login/callback', None,\n            'Downloading callback page',\n            data=urlencode_postdata(self._hidden_inputs(login_page)),\n            headers={\n                'Content-Type': 'application/x-www-form-urlencoded',\n                'Origin': 'https://login.linuxacademy.com',\n                'Referer': login_state_url,\n            })\n\n        access_token = self._search_regex(\n            r'access_token=([^=&]+)', urlh.geturl(),\n            'access token', default=None)\n        if not access_token:\n            access_token = self._parse_json(","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/ytdl-org/youtube-dl/blob/956b8c585591b401a543e409accb163eeaaa1193/youtube_dl/extractor/linuxacademy.py#L108-L144","documentation":"Raised by the Linux Academy extractor when the login endpoint returns HTTP 401: the JSON error body's 'description' (or 'code') becomes the message alongside the extractor name. It is expected=True and means the login request itself was rejected — bad credentials or an expired auth flow.","triggerScenarios":"Using --username/--password with linuxacademy.com URLs where POST https://login.linuxacademy.com/usernamepassword/login responds 401 with a JSON error — wrong credentials, stale login state url, or the auth flow (state/nonce handshake) having changed.","commonSituations":"Incorrect email/password; Linux Academy (now Cloud Assessment/TAC Security) changing their auth stack so the two-step login dance breaks; MFA required on the account; old youtube-dl versions predating an auth-flow update.","solutions":["Verify credentials in a browser first","Update youtube-dl / switch to yt-dlp — the login handshake is version-sensitive","If MFA blocks headless login, use --cookies with an exported authenticated session","Read the embedded 'description' text; it usually names the exact refusal reason"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    ydl.download([url])\nexcept ExtractorError as e:\n    if ' said: ' in str(e):\n        # login rejected (401): fall back to exported session cookies\n        switch_to_cookie_auth_and_retry(url)\n    else:\n        raise","preventionTips":["Verify credentials in a browser before scripted login","Update extractors before debugging — auth-flow breakage is usually fixed upstream","Use cookie-based auth when MFA is enabled on the account"],"tags":["linuxacademy","authentication","http-401","login","extractor"],"backgroundTag":null,"analyzedSha":"956b8c585591b401a543e409accb163eeaaa1193","analyzedAt":"2026-08-14T18:59:47.863Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}