@@ -136,13 +136,24 @@ def __get_authorization_code(self, client, auth_url, scope, state, challenge):
136136 handler = OAuthHttpSingleRequestHandler ("Databricks Sql Connector" )
137137
138138 last_error = None
139+ callback_timed_out = False
139140 for port in self .port_range :
140141 try :
141142 with HTTPServer (("" , port ), handler ) as httpd :
142143 # Bound how long we wait for the browser redirect callback so
143144 # that a headless environment (no browser to complete the
144145 # flow) fails with a clear error instead of hanging forever.
145146 httpd .timeout = self .REDIRECT_CALLBACK_TIMEOUT_SECONDS
147+
148+ # HTTPServer.handle_request() returns normally (via
149+ # handle_timeout()) when the wait elapses without a
150+ # connection, so record that case to distinguish it from a
151+ # received-but-empty callback below.
152+ def _on_timeout ():
153+ nonlocal callback_timed_out
154+ callback_timed_out = True
155+
156+ httpd .handle_timeout = _on_timeout
146157 redirect_url = OAuthManager .__get_redirect_url (port )
147158 auth_req_uri , _ , _ = client .prepare_authorization_request (
148159 authorization_url = auth_url ,
@@ -174,7 +185,16 @@ def __get_authorization_code(self, client, auth_url, scope, state, challenge):
174185 raise last_error
175186
176187 if not handler .request_path :
177- msg = f"No path parameters were returned to the callback at { redirect_url } "
188+ if callback_timed_out :
189+ msg = (
190+ f"Timed out after { self .REDIRECT_CALLBACK_TIMEOUT_SECONDS } "
191+ f"seconds waiting for the OAuth redirect callback at "
192+ f"{ redirect_url } . No browser completed the login flow — this "
193+ "is expected in a headless environment (e.g. a notebook or "
194+ "job with no browser). See issue #458."
195+ )
196+ else :
197+ msg = f"No path parameters were returned to the callback at { redirect_url } "
178198 logger .error (msg )
179199 raise RuntimeError (msg )
180200 # This is a kludge because the parsing library expects https callbacks
0 commit comments