Repository: encode/uvicorn

Issue:
Include a environment variable to change arg commands
Hello,

I'm testing the FASTAPI (https://fastapi.tiangolo.com/) and there's a nice environment variable when running with gunicorn (https://docs.gunicorn.org/en/stable/settings.html#settings) `GUNICORN_CMD_ARGS`, it include command line args using enviroment variables

this is very usefull when using docker compose, or k8s. without rebuild the container i can change how it starts (more debug messages instead of only critical)

could be possible include a enviroment at uvicorn? `UVICORN_CMD_ARGS`

the gunicorn_cmd_args implementation can be saw here:

https://github.com/benoitc/gunicorn/blob/ee685e197b3f7cf899dc7d6e0688ff169e9d10df/gunicorn/app/base.py#L171

https://github.com/benoitc/gunicorn/blob/6aab4decde5735fc77daf4fecaf9ef3632189f62/gunicorn/config.py#L79


thanks!

Discussion:
Kludex: @euri10 ping

Patch:
diff --git a/.github/ISSUE_TEMPLATE/2-bug-report.md b/.github/ISSUE_TEMPLATE/2-bug-report.md
deleted file mode 100644
index a5eccd2..0000000
--- a/.github/ISSUE_TEMPLATE/2-bug-report.md
+++ /dev/null
@@ -1,53 +0,0 @@
----
-name: Bug report
-about: Report a bug to help improve this project
----
-
-### Checklist
-
-<!-- Please make sure you check all these items before submitting your bug report. -->
-
-- [ ] The bug is reproducible against the latest release and/or `master`.
-- [ ] There are no similar issues or pull requests to fix it yet.
-
-### Describe the bug
-
-<!-- A clear and concise description of what the bug is. -->
-
-### To reproduce
-
-<!-- Provide a *minimal* example with steps to reproduce the bug locally.
-
-NOTE: try to keep any external dependencies *at an absolute minimum* .
-In other words, remove anything that doesn't make the bug go away.
-
--->
-
-### Expected behavior
-
-<!-- A clear and concise description of what you expected to happen. -->
-
-### Actual behavior
-
-<!-- A clear and concise description of what actually happens. -->
-
-### Debugging material
-
-<!-- Any tracebacks, screenshots, etc. that can help understanding the problem.
-
-NOTE:
-- Please list tracebacks in full (don't truncate them).
-- If relevant, consider turning on DEBUG or TRACE logs for additional details (see the Logging section on https://www.uvicorn.org/settings/ specifically the `log-level` flag).
-- Consider using `<details>` to make tracebacks/logs collapsible if they're very large (see https://gist.github.com/ericclemmons/b146fe5da72ca1f706b2ef72a20ac39d).
--->
-
-### Environment
-
-- OS / Python / Uvicorn version: just run `uvicorn --version`
-- The exact command you're running uvicorn with, all flags you passed included. If you run it with gunicorn please do the same. If there is a reverse-proxy involved and you cannot reproduce without it please give the minimal config of it to reproduce.
-
-### Additional context
-
-<!-- Any additional information that can help understanding the problem.
-
-Eg. linked issues, or a description of what you were trying to achieve. -->
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/3-feature-request.md b/.github/ISSUE_TEMPLATE/3-feature-request.md
deleted file mode 100644
index d4ef094..0000000
--- a/.github/ISSUE_TEMPLATE/3-feature-request.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project.
----
-
-### Checklist
-
-<!-- Please make sure you check all these items before submitting your feature request. -->
-
-- [ ] There are no similar issues or pull requests for this yet.
-- [ ] I discussed this idea on the [community chat](https://gitter.im/encode/community) and feedback is positive.
-
-### Is your feature related to a problem? Please describe.
-
-<!-- A clear and concise description of what you are trying to achieve.
-
-Eg "I want to be able to [...] but I can't because [...]". -->
-
-## Describe the solution you would like.
-
-<!-- A clear and concise description of what you would want to happen.
-
-For API changes, try to provide a code snippet of what you would like the API to look like.
--->
-
-## Describe alternatives you considered
-
-<!-- Please describe any alternative solutions or features you've considered to solve
-your problem and why they wouldn't solve it. -->
-
-## Additional context
-
-<!-- Provide any additional context, screenshots, tracebacks, etc. about the feature here. -->
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
new file mode 100644
index 0000000..4f3f2df
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -0,0 +1,75 @@
+name: Bug report
+description: Report a bug to help improve this project
+body:
+  - type: checkboxes
+    id: checks
+    attributes:
+      label: Checklist
+      description: Please make sure you check all these items before submitting your bug report.
+      options:
+        - label: The bug is reproducible against the latest release or `master`.
+          required: true
+        - label: There are no similar issues or pull requests to fix it yet.
+          required: true
+  - type: textarea
+    id: describe
+    attributes:
+      label: Describe the bug
+      description: A clear and concise description of what the bug is.
+    validations:
+      required: true
+  - type: textarea
+    id: steps
+    attributes:
+      label: Steps to reproduce the bug
+      description: |
+        Provide a *minimal* example with steps to reproduce the bug locally.
+        NOTE: try to keep any external dependencies *at an absolute minimum*.
+        In other words, remove anything that doesn't make the bug go away.
+    validations:
+      required: false
+  - type: textarea
+    id: expected
+    attributes:
+      label: Expected behavior
+      description: A clear and concise description of what you expected to happen.
+    validations:
+      required: false
+  - type: textarea
+    id: actual
+    attributes:
+      label: Actual behavior
+      description: A clear and concise description of what actually happened.
+    validations:
+      required: false
+  - type: textarea
+    id: notes
+    attributes:
+      label: Debugging material
+      description: |
+        Any tracebacks, screenshots, etc. that can help understanding the problem.
+        NOTE:
+        - Please list tracebacks in full (don't truncate them).
+        - If relevant, consider turning on DEBUG or TRACE logs for additional details (see the Logging section on https://www.uvicorn.org/settings/ specifically the `log-level` flag).
+        - Consider using `<details>` to make tracebacks/logs collapsible if they're very large (see https://gist.github.com/ericclemmons/b146fe5da72ca1f706b2ef72a20ac39d).
+    validations:
+      required: false
+  - type: textarea
+    id: environment
+    attributes:
+      label: Environment
+      description: Describe your environment.
+      placeholder: |
+        - OS / Python / Uvicorn version: just run `uvicorn --version`
+        - The exact command you're running uvicorn with, all flags you passed included. If you run it with gunicorn please do the same. If there is a reverse-proxy involved and you cannot reproduce without it please give the minimal config of it to reproduce.
+    validations:
+      required: true
+  - type: textarea
+    id: additional
+    attributes:
+      label: Additional context
+      description: |
+        Any additional information that can help understanding the problem.
+        Eg. linked issues, or a description of what you were trying to achieve.
+    validations:
+      required: false
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
new file mode 100644
index 0000000..7ee6c77
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.yml
@@ -0,0 +1,36 @@
+name: Feature Request
+description: Suggest an idea for a feature that you would like to see in uvicorn
+body:
+  - type: checkboxes
+    id: checks
+    attributes:
+      label: Checklist
+      description: Please make sure you check all these items before submitting your feature request.
+      options:
+        - label: There are no similar issues or pull requests for this yet.
+          required: true
+        - label: I discussed this idea on the [community chat](https://gitter.im/encode/community) and feedback is positive.
+          required: true
+  - type: textarea
+    id: problem
+    attributes:
+      label: Is your feature related to a problem? Please describe.
+      description: A clear and concise description of what you are trying to achieve.
+      placeholder: I want to be able to [...] but I can't because [...]
+  - type: textarea
+    id: solution
+    attributes:
+      label: Describe the solution you would like.
+      description: |
+        A clear and concise description of what you would want to happen.
+        For API changes, try to provide a code snippet of what you would like the API to look like.
+  - type: textarea
+    id: alternatives
+    attributes:
+      label: Describe alternatives you considered
+      description: Please describe any alternative solutions or features you've considered to solve your problem and why they wouldn't solve it.
+  - type: textarea
+    id: additional
+    attributes:
+      label: Additional context
+      description: Provide any additional context, screenshots, trace backs, etc. about the feature here.
diff --git a/docs/settings.md b/docs/settings.md
index b9055a9..3edf4b3 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -7,6 +7,13 @@ equivalent keyword arguments, eg. `uvicorn.run("example:app", port=5000, reload=
 Please note that in this case, if you use `reload=True` or `workers=NUM`,
 you should put `uvicorn.run` into `if __name__ == '__main__'` clause in the main module.
 
+You can also configure Uvicorn using environment variables with the prefix `UVICORN_`.
+For example, in case you want to run the app on port `5000`, just set the environment variable `UVICORN_PORT` to `5000`.
+
+!!! note
+    CLI options and the arguments for `uvicorn.run()` take precedence over environment variables.
+
+
 ## Application
 
 * `APP` - The ASGI application to run, in the format `"<module>:<attribute>"`.
diff --git a/mkdocs.yml b/mkdocs.yml
index 040e8cd..ed833a1 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -29,6 +29,7 @@ nav:
   - Contributing: "contributing.md"
 
 markdown_extensions:
+  - admonition
   - codehilite:
       css_class: highlight
   - toc:
diff --git a/uvicorn/main.py b/uvicorn/main.py
index c4f4a6c..95174d3 100644
--- a/uvicorn/main.py
+++ b/uvicorn/main.py
@@ -30,6 +30,8 @@ LIFESPAN_CHOICES = click.Choice(list(LIFESPAN.keys()))
 LOOP_CHOICES = click.Choice([key for key in LOOP_SETUPS.keys() if key != "none"])
 INTERFACE_CHOICES = click.Choice(INTERFACES)
 
+STARTUP_FAILURE = 3
+
 logger = logging.getLogger("uvicorn.error")
 
 
@@ -48,7 +50,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
     ctx.exit()
 
 
-@click.command()
+@click.command(context_settings={"auto_envvar_prefix": "UVICORN"})
 @click.argument("app")
 @click.option(
     "--host",
@@ -449,6 +451,9 @@ def run(app: typing.Union[ASGIApplication, str], **kwargs: typing.Any) -> None:
     if config.uds:
         os.remove(config.uds)  # pragma: py-win32
 
+    if not server.started and not config.should_reload and config.workers == 1:
+        sys.exit(STARTUP_FAILURE)
+
 
 if __name__ == "__main__":
     main()  # pragma: no cover


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index e372c7e..4df47b4 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,4 +1,5 @@
 import importlib
+import os
 import platform
 import sys
 from pathlib import Path
@@ -17,6 +18,10 @@ HEADERS = "Content-Security-Policy:default-src 'self'; script-src https://exampl
 main = importlib.import_module("uvicorn.main")
 
 
+class App:
+    pass
+
+
 def test_cli_print_version() -> None:
     runner = CliRunner()
 
@@ -57,7 +62,7 @@ def test_cli_call_server_run() -> None:
     with mock.patch.object(Server, "run") as mock_run:
         result = runner.invoke(cli, ["tests.test_cli:App"])
 
-    assert result.exit_code == 0
+    assert result.exit_code == 3
     mock_run.assert_called_once()
 
 
@@ -131,5 +136,26 @@ def test_cli_reloader_incomplete_app_parameter(
     ) in captured.err
 
 
-class App:
-    pass
+@pytest.fixture()
+def load_env_h11_protocol():
+    old_environ = dict(os.environ)
+    os.environ["UVICORN_HTTP"] = "h11"
+    yield
+    os.environ.clear()
+    os.environ.update(old_environ)
+
+
+def test_env_variables(load_env_h11_protocol: None):
+    runner = CliRunner(env=os.environ)
+    with mock.patch.object(main, "run") as mock_run:
+        runner.invoke(cli, ["tests.test_cli:App"])
+        _, kwargs = mock_run.call_args
+        assert kwargs["http"] == "h11"
+
+
+def test_mistmatch_env_variables(load_env_h11_protocol: None):
+    runner = CliRunner(env=os.environ)
+    with mock.patch.object(main, "run") as mock_run:
+        runner.invoke(cli, ["tests.test_cli:App", "--http=httptools"])
+        _, kwargs = mock_run.call_args
+        assert kwargs["http"] == "httptools"
diff --git a/tests/test_main.py b/tests/test_main.py
index 9f96620..2b5a124 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -74,3 +74,15 @@ def test_run_invalid_app_config_combination(caplog: pytest.LogCaptureFixture) ->
         "You must pass the application as an import string to enable "
         "'reload' or 'workers'."
     )
+
+
+def test_run_startup_failure(caplog: pytest.LogCaptureFixture) -> None:
+    async def app(scope, receive, send):
+        assert scope["type"] == "lifespan"
+        message = await receive()
+        if message["type"] == "lifespan.startup":
+            raise RuntimeError("Startup failed")
+
+    with pytest.raises(SystemExit) as exit_exception:
+        run(app, lifespan="on")
+    assert exit_exception.value.code == 3