Skip to content

Commit

Permalink
Better DNS error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
isidentical committed Dec 22, 2021
1 parent 5a83a9e commit def9f85
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
- Added support for basic JSON types on `--form`/`--multipart` when using JSON only operators (`:=`/`:=@`). ([#1212](https://github.com/httpie/httpie/issues/1212))
- Added support for automatically enabling `--stream` when `Content-Type` is `text/event-stream`. ([#376](https://github.com/httpie/httpie/issues/376))
- Added new `pie-dark`/`pie-light` (and `pie`) styles that match with [HTTPie for Web and Desktop](https://httpie.io/product). ([#1237](https://github.com/httpie/httpie/issues/1237))
- Added support for better error handling on DNS failures. ([#1248](https://github.com/httpie/httpie/issues/1248))
- Broken plugins will no longer crash the whole application. ([#1204](https://github.com/httpie/httpie/issues/1204))
- Fixed auto addition of XML declaration to every formatted XML response. ([#1156](https://github.com/httpie/httpie/issues/1156))
- Fixed highlighting when `Content-Type` specifies `charset`. ([#1242](https://github.com/httpie/httpie/issues/1242))
Expand Down
43 changes: 32 additions & 11 deletions httpie/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import platform
import sys
import socket
from typing import List, Optional, Tuple, Union, Callable

import requests
Expand All @@ -21,6 +22,7 @@
from .output.writer import write_message, write_stream, MESSAGE_SEPARATOR_BYTES
from .plugins.registry import plugin_manager
from .status import ExitStatus, http_status_to_exit_status
from .utils import unwrap_context


# noinspection PyDefaultArgument
Expand All @@ -41,6 +43,21 @@ def raw_main(
include_debug_info = '--debug' in args
include_traceback = include_debug_info or '--traceback' in args

def handle_generic_error(e, annotation=None):
msg = str(e)
if hasattr(e, 'request'):
request = e.request
if hasattr(request, 'url'):
msg = (
f'{msg} while doing a {request.method}'
f' request to URL: {request.url}'
)
if annotation:
msg += annotation
env.log_error(f'{type(e).__name__}: {msg}')
if include_traceback:
raise

if include_debug_info:
print_debug_info(env)
if args == ['--debug']:
Expand Down Expand Up @@ -90,19 +107,23 @@ def raw_main(
f'Too many redirects'
f' (--max-redirects={parsed_args.max_redirects}).'
)
except requests.exceptions.ConnectionError as exc:
annotation = None
original_exc = unwrap_context(exc)
if isinstance(original_exc, socket.gaierror):
if original_exc.errno == socket.EAI_AGAIN:
annotation = '\nCouldn\'t connect to a DNS server. Perhaps check your connection and try again.'
elif original_exc.errno == socket.EAI_NONAME:
annotation = '\nCouldn\'t resolve the given URL. Perhaps check the URL and try again.'
propagated_exc = original_exc
else:
propagated_exc = exc

handle_generic_error(propagated_exc, annotation=annotation)
exit_status = ExitStatus.ERROR
except Exception as e:
# TODO: Further distinction between expected and unexpected errors.
msg = str(e)
if hasattr(e, 'request'):
request = e.request
if hasattr(request, 'url'):
msg = (
f'{msg} while doing a {request.method}'
f' request to URL: {request.url}'
)
env.log_error(f'{type(e).__name__}: {msg}')
if include_traceback:
raise
handle_generic_error(e)
exit_status = ExitStatus.ERROR

return exit_status
Expand Down
8 changes: 8 additions & 0 deletions httpie/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,11 @@ def split(iterable: Iterable[T], key: Callable[[T], bool]) -> Tuple[List[T], Lis
else:
right.append(item)
return left, right


def unwrap_context(exc: Exception) -> Optional[Exception]:
context = exc.__context__
if isinstance(context, Exception):
return unwrap_context(context)
else:
return exc
17 changes: 17 additions & 0 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest
import socket
from unittest import mock
from pytest import raises
from requests import Request
Expand Down Expand Up @@ -31,6 +33,21 @@ def test_error_traceback(program):
http('--traceback', 'www.google.com')


@mock.patch('httpie.core.program')
@pytest.mark.parametrize("error_code, expected_message", [
(socket.EAI_AGAIN, "check your connection"),
(socket.EAI_NONAME, "check the URL"),
])
def test_error_custom_dns(program, error_code, expected_message):
exc = ConnectionError('Connection aborted')
exc.__context__ = socket.gaierror(error_code, "<test>")
program.side_effect = exc

r = http('www.google.com', tolerate_error_exit_status=True)
assert r.exit_status == ExitStatus.ERROR
assert expected_message in r.stderr


def test_max_headers_limit(httpbin_both):
with raises(ConnectionError) as e:
http('--max-headers=1', httpbin_both + '/get')
Expand Down

0 comments on commit def9f85

Please sign in to comment.