From 2646ebaaed81a53303c7fb60a6b50452c81bc025 Mon Sep 17 00:00:00 2001 From: Jakub Roztocil Date: Tue, 24 Jul 2012 01:09:14 +0200 Subject: [PATCH] Replaced --ignore-http-status with --check-status. The default behaviour now is to exit with 0 on HTTP errors unless --check-status is set. --- README.rst | 54 ++++++++++++++++++++++------ httpie/cli.py | 22 +++++++----- httpie/core.py | 18 +++++++--- httpie/models.py | 2 ++ tests/tests.py | 93 +++++++++++++++++++++++++++++++++++------------- 5 files changed, 140 insertions(+), 49 deletions(-) diff --git a/README.rst b/README.rst index 4068ebca..7430f1bd 100644 --- a/README.rst +++ b/README.rst @@ -151,6 +151,11 @@ the second one does via ``stdin``:: http https://api.github.com/repos/jkbr/httpie | http httpbin.org/post +Note that when the output is redirected (like the examples above), HTTPie +applies a different set of defaults then for console output. Namely colors +aren't used (can be forced with ``--pretty``) and only the response body +gets printed (can be overwritten with ``--print``). + An alternative to ``stdin`` is to pass a file name whose content will be used as the request body. It has the advantage that the ``Content-Type`` header will automatically be set to the appropriate value based on the filename @@ -160,6 +165,25 @@ request will send the verbatim contents of the file with http PUT httpbin.org/put @/data/file.xml +When using HTTPie from shell scripts you might want to use the +``--check-status`` flag. It instructs HTTPie to exit with an error if the +HTTP status is one of ``3xx``, ``4xx``, or ``5xx``. The exit status will +be ``3`` (unless ``--allow-redirects`` is set), ``4``, or ``5`` +respectivelly:: + + #!/bin/bash + + if http --check-status example.org/health &> /dev/null; then + echo 'OK!' + else + case $? in + 3) echo 'Unexpected 3xx Redirection!' ;; + 4) echo '4xx Client Error!' ;; + 5) echo '5xx Server Error!' ;; + *) echo 'Other Error!' ;; + esac + fi + Flags ^^^^^ @@ -170,7 +194,7 @@ See ``http -h`` for more details:: usage: http [-h] [--version] [--json | --form] [--traceback] [--pretty | --ugly] [--print OUTPUT_OPTIONS | --verbose | --headers | --body] - [--style STYLE] [--ignore-http-status] [--auth AUTH] + [--style STYLE] [--check-status] [--auth AUTH] [--auth-type {basic,digest}] [--verify VERIFY] [--proxy PROXY] [--allow-redirects] [--timeout TIMEOUT] [METHOD] URL [ITEM [ITEM ...]] @@ -235,14 +259,16 @@ See ``http -h`` for more details:: make sure that the $TERM environment variable is set to "xterm-256color" or similar (e.g., via `export TERM =xterm-256color' in your ~/.bashrc). - --ignore-http-status This flag tells HTTP to ignore the HTTP status code - and exit with 0. By default, HTTPie exits with 0 only - if no errors occur and the request is successful. When - the server replies with a 4xx (Client Error) or 5xx - (Server Error) status code, HTTPie exits with 4 or 5 - respectively. If the response is 3xx (Redirect) and - --allow-redirects isn't set, then the exit status is - 3. + --check-status By default, HTTPie exits with 0 when no network or + other fatal errors occur. This flag instructs HTTPie + to also check the HTTP status code and exit with an + error if the status indicates one. When the server + replies with a 4xx (Client Error) or 5xx (Server + Error) status code, HTTPie exits with 4 or 5 + respectively. If the response is a 3xx (Redirect) and + --allow-redirects hasn't been set, then the exit + status is 3. Also an error message is written to + stderr if stdout is redirected. --auth AUTH, -a AUTH username:password. If only the username is provided (-a username), HTTPie will prompt for the password. --auth-type {basic,digest} @@ -261,6 +287,7 @@ See ``http -h`` for more details:: socket.setdefaulttimeout() as fallback). + Contribute ----------- @@ -276,8 +303,13 @@ to see if the feature/bug has previously been discussed. Then fork develop branch and submit a pull request. Note: Pull requests with tests and documentation are 53.6% more awesome :) -Before a pull requests is submitted, it's a good idea to run the existing -suite of tests:: +To point the ``http`` command to your working copy you can install HTTPie in +the editable mode:: + + pip install --editable . + +It's a good idea to run the existing suite of tests before a pull requests is +submitted:: python setup.py test diff --git a/httpie/cli.py b/httpie/cli.py index 5c1b9610..fb77b8ca 100644 --- a/httpie/cli.py +++ b/httpie/cli.py @@ -126,17 +126,21 @@ parser.add_argument( ) parser.add_argument( - '--ignore-http-status', default=False, action='store_true', + '--check-status', default=False, action='store_true', help=_(''' - This flag tells HTTP to ignore the HTTP status code - and exit with 0. + By default, HTTPie exits with 0 when no network or other fatal + errors occur. + + This flag instructs HTTPie to also check the HTTP status code and + exit with an error if the status indicates one. + + When the server replies with a 4xx (Client Error) or 5xx + (Server Error) status code, HTTPie exits with 4 or 5 respectively. + If the response is a 3xx (Redirect) and --allow-redirects + hasn't been set, then the exit status is 3. + + Also an error message is written to stderr if stdout is redirected. - By default, HTTPie exits with 0 only if no errors occur - and the request is successful. When the server - replies with a 4xx (Client Error) or 5xx (Server Error) - status code, HTTPie exits with 4 or 5 respectively. - If the response is 3xx (Redirect) and --allow-redirects - isn't set, then the exit status is 3. ''') ) diff --git a/httpie/core.py b/httpie/core.py index 52711834..b2b7c121 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -141,12 +141,20 @@ def main(args=sys.argv[1:], env=Environment()): """ args = cli.parser.parse_args(args=args, env=env) response = get_response(args) + + status = 0 + + if args.check_status: + status = get_exist_status(response.status_code, + args.allow_redirects) + if status and not env.stdout_isatty: + err = 'http error: %s %s\n' % ( + response.raw.status, response.raw.reason) + env.stderr.write(err.encode('utf8')) + output = get_output(args, env, response) output_bytes = output.encode('utf8') f = getattr(env.stdout, 'buffer', env.stdout) f.write(output_bytes) - if args.ignore_http_status: - return 0 - else: - return get_exist_status( - response.status_code, args.allow_redirects) + + return status diff --git a/httpie/models.py b/httpie/models.py index 80fcec50..0aa2209d 100644 --- a/httpie/models.py +++ b/httpie/models.py @@ -17,6 +17,8 @@ class Environment(object): stdout_isatty = sys.stdout.isatty() stdout = sys.stdout + stderr = sys.stderr + # Can be set to 0 to disable colors completely. colors = 256 if '256color' in os.environ.get('TERM', '') else 88 diff --git a/tests/tests.py b/tests/tests.py index eac1c7b0..ad397897 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -53,10 +53,12 @@ def httpbin(path): class Response(str): """ - A unicode subclass holding the output of `main()` and the exit status. + A unicode subclass holding the output of `main()`, and also + the exit status and contents of ``stderr``. """ exit_status = None + stderr = None def http(*args, **kwargs): @@ -75,17 +77,21 @@ def http(*args, **kwargs): ) stdout = kwargs['env'].stdout = tempfile.TemporaryFile() + stderr = kwargs['env'].stderr = tempfile.TemporaryFile() exit_status = main(args=args, **kwargs) stdout.seek(0) + stderr.seek(0) - response = Response(stdout.read().decode('utf8')) - response.exit_status = exit_status + r = Response(stdout.read().decode('utf8')) + r.stderr = stderr.read().decode('utf8') + r.exit_status = exit_status stdout.close() + stderr.close() - return response + return r class BaseTestCase(unittest.TestCase): @@ -155,6 +161,7 @@ class HTTPieTest(BaseTestCase): env = Environment( stdin=open(TEST_FILE_PATH), stdin_isatty=False, + stdout_isatty=True, colors=0, ) @@ -276,7 +283,10 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase): r = http( 'GET', httpbin('/get'), - env=Environment(stdout_isatty=False) + env=Environment( + stdin_isatty=True, + stdout_isatty=False + ) ) self.assertNotIn('HTTP/', r) @@ -286,7 +296,10 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase): '--print=h', 'GET', httpbin('/get'), - env=Environment(stdout_isatty=False) + env=Environment( + stdin_isatty=True, + stdout_isatty=False + ) ) self.assertIn('HTTP/1.1 200', r) @@ -326,6 +339,7 @@ class ImplicitHTTPMethodTest(BaseTestCase): env = Environment( stdin_isatty=False, stdin=open(TEST_FILE_PATH), + stdout_isatty=True, colors=0, ) r = http( @@ -343,7 +357,10 @@ class PrettyFlagTest(BaseTestCase): r = http( 'GET', httpbin('/get'), - env=Environment(stdout_isatty=True), + env=Environment( + stdin_isatty=True, + stdout_isatty=True, + ), ) self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r) @@ -359,7 +376,10 @@ class PrettyFlagTest(BaseTestCase): '--pretty', 'GET', httpbin('/get'), - env=Environment(stdout_isatty=False), + env=Environment( + stdin_isatty=True, + stdout_isatty=False + ), ) self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r) @@ -508,16 +528,40 @@ class AuthTest(BaseTestCase): class ExitStatusTest(BaseTestCase): - def test_3xx_exits_3(self): + def test_ok_response_exits_0(self): r = http( 'GET', - httpbin('/status/301') + httpbin('/status/200') + ) + self.assertIn('HTTP/1.1 200', r) + self.assertEqual(r.exit_status, 0) + + def test_error_response_exits_0_without_check_status(self): + r = http( + 'GET', + httpbin('/status/500') + ) + self.assertIn('HTTP/1.1 500', r) + self.assertEqual(r.exit_status, 0) + + def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(self): + r = http( + '--check-status', + '--headers', # non-terminal, force headers + 'GET', + httpbin('/status/301'), + env=Environment( + stdout_isatty=False, + stdin_isatty=True, + ) ) self.assertIn('HTTP/1.1 301', r) self.assertEqual(r.exit_status, 3) + self.assertIn('301 moved permanently', r.stderr.lower()) - def test_3xx_redirects_allowed_exits_0(self): + def test_3xx_check_status_redirects_allowed_exits_0(self): r = http( + '--check-status', '--allow-redirects', 'GET', httpbin('/status/301') @@ -526,31 +570,26 @@ class ExitStatusTest(BaseTestCase): self.assertIn('HTTP/1.1 200 OK', r) self.assertEqual(r.exit_status, 0) - def test_4xx_exits_4(self): + def test_4xx_check_status_exits_4(self): r = http( + '--check-status', 'GET', httpbin('/status/401') ) self.assertIn('HTTP/1.1 401', r) self.assertEqual(r.exit_status, 4) + # Also stderr should be empty since stdout isn't redirected. + self.assert_(not r.stderr) - def test_5xx_exits_5(self): + def test_5xx_check_status_exits_5(self): r = http( + '--check-status', 'GET', httpbin('/status/500') ) self.assertIn('HTTP/1.1 500', r) self.assertEqual(r.exit_status, 5) - def test_ignore_http_status_exits_0(self): - r = http( - '--ignore-http-status', - 'GET', - httpbin('/status/500') - ) - self.assertIn('HTTP/1.1 500', r) - self.assertEqual(r.exit_status, 0) - ################################################################# # CLI argument parsing related tests. @@ -660,7 +699,10 @@ class ArgumentParserTestCase(unittest.TestCase): args.url = 'http://example.com/' args.items = [] - self.parser._guess_method(args, Environment()) + self.parser._guess_method(args, Environment( + stdin_isatty=True, + stdout_isatty=True, + )) self.assertEquals(args.method, 'GET') self.assertEquals(args.url, 'http://example.com/') @@ -687,7 +729,10 @@ class ArgumentParserTestCase(unittest.TestCase): args.url = 'test:header' args.items = [] - self.parser._guess_method(args, Environment()) + self.parser._guess_method(args, Environment( + stdin_isatty=True, + stdout_isatty=True, + )) self.assertEquals(args.method, 'GET') self.assertEquals(args.url, 'http://example.com/')