Repository: docker/docker-py

Issue:
build images with tag with https prefix never ends
## Reproduces

```
docker_client = docker.from_env()
docker_client .images.build(path="{my-path}", tag="https://dummy:latest", dockerfile="{dockerfile-path})
```

Using the debugger, we can see it loop on the _post request here:

https://github.com/docker/docker-py/blob/84414e343e526cf93f285284dd2c2c40f703e4a9/docker/api/build.py#L264-L271



Discussion:
https://dummy:latest is not a valid image tag

Docker image tags can only contain lowercase and uppercase letters, digits, underscores, periods, and dashes. https://docs.docker.com/engine/reference/commandline/tag/

if you are trying to use custom registry, there's no need to add https prefix

Resolving patch:
diff --git a/docker/api/build.py b/docker/api/build.py
--- a/docker/api/build.py
+++ b/docker/api/build.py
@@ -129,13 +129,16 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None,
             raise errors.DockerException(
                 'Can not use custom encoding if gzip is enabled'
             )
-
+        if tag is not None:
+            if not utils.match_tag(tag):
+                raise errors.DockerException(
+                    f"invalid tag '{tag}': invalid reference format"
+            )
         for key in container_limits.keys():
             if key not in constants.CONTAINER_LIMITS_KEYS:
                 raise errors.DockerException(
-                    f'Invalid container_limits key {key}'
+                    f"invalid tag '{tag}': invalid reference format"
                 )
-
         if custom_context:
             if not fileobj:
                 raise TypeError("You must specify fileobj with custom_context")
diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py
--- a/docker/utils/__init__.py
+++ b/docker/utils/__init__.py
@@ -1,5 +1,5 @@
 
-from .build import create_archive, exclude_paths, mkbuildcontext, tar
+from .build import match_tag, create_archive, exclude_paths, mkbuildcontext, tar
 from .decorators import check_resource, minimum_version, update_headers
 from .utils import (
     compare_version, convert_port_bindings, convert_volume_binds,
diff --git a/docker/utils/build.py b/docker/utils/build.py
--- a/docker/utils/build.py
+++ b/docker/utils/build.py
@@ -9,6 +9,14 @@
 
 
 _SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/')
+_TAG = re.compile(
+    r"^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*" \
+        + "(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127})?$"
+)
+
+
+def match_tag(tag: str) -> bool:
+    return bool(_TAG.match(tag))
 
 
 def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):


Test patch:
diff --git a/tests/unit/api_build_test.py b/tests/unit/api_build_test.py
--- a/tests/unit/api_build_test.py
+++ b/tests/unit/api_build_test.py
@@ -2,181 +2,206 @@
 import io
 import shutil
 
+import pytest
+
 import docker
-from docker import auth
+from docker import auth, errors
 from docker.api.build import process_dockerfile
 
-import pytest
-
 from ..helpers import make_tree
 from .api_test import BaseAPIClientTest, fake_request, url_prefix
 
 
 class BuildTest(BaseAPIClientTest):
     def test_build_container(self):
-        script = io.BytesIO('\n'.join([
-            'FROM busybox',
-            'RUN mkdir -p /tmp/test',
-            'EXPOSE 8080',
-            'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz'
-            ' /tmp/silence.tar.gz'
-        ]).encode('ascii'))
+        script = io.BytesIO(
+            "\n".join(
+                [
+                    "FROM busybox",
+                    "RUN mkdir -p /tmp/test",
+                    "EXPOSE 8080",
+                    "ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz"
+                    " /tmp/silence.tar.gz",
+                ]
+            ).encode("ascii")
+        )
 
         self.client.build(fileobj=script)
 
     def test_build_container_pull(self):
-        script = io.BytesIO('\n'.join([
-            'FROM busybox',
-            'RUN mkdir -p /tmp/test',
-            'EXPOSE 8080',
-            'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz'
-            ' /tmp/silence.tar.gz'
-        ]).encode('ascii'))
+        script = io.BytesIO(
+            "\n".join(
+                [
+                    "FROM busybox",
+                    "RUN mkdir -p /tmp/test",
+                    "EXPOSE 8080",
+                    "ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz"
+                    " /tmp/silence.tar.gz",
+                ]
+            ).encode("ascii")
+        )
 
         self.client.build(fileobj=script, pull=True)
 
     def test_build_container_custom_context(self):
-        script = io.BytesIO('\n'.join([
-            'FROM busybox',
-            'RUN mkdir -p /tmp/test',
-            'EXPOSE 8080',
-            'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz'
-            ' /tmp/silence.tar.gz'
-        ]).encode('ascii'))
+        script = io.BytesIO(
+            "\n".join(
+                [
+                    "FROM busybox",
+                    "RUN mkdir -p /tmp/test",
+                    "EXPOSE 8080",
+                    "ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz"
+                    " /tmp/silence.tar.gz",
+                ]
+            ).encode("ascii")
+        )
         context = docker.utils.mkbuildcontext(script)
 
         self.client.build(fileobj=context, custom_context=True)
 
     def test_build_container_custom_context_gzip(self):
-        script = io.BytesIO('\n'.join([
-            'FROM busybox',
-            'RUN mkdir -p /tmp/test',
-            'EXPOSE 8080',
-            'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz'
-            ' /tmp/silence.tar.gz'
-        ]).encode('ascii'))
+        script = io.BytesIO(
+            "\n".join(
+                [
+                    "FROM busybox",
+                    "RUN mkdir -p /tmp/test",
+                    "EXPOSE 8080",
+                    "ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz"
+                    " /tmp/silence.tar.gz",
+                ]
+            ).encode("ascii")
+        )
         context = docker.utils.mkbuildcontext(script)
         gz_context = gzip.GzipFile(fileobj=context)
 
-        self.client.build(
-            fileobj=gz_context,
-            custom_context=True,
-            encoding="gzip"
-        )
+        self.client.build(fileobj=gz_context, custom_context=True, encoding="gzip")
 
     def test_build_remote_with_registry_auth(self):
-        self.client._auth_configs = auth.AuthConfig({
-            'auths': {
-                'https://example.com': {
-                    'user': 'example',
-                    'password': 'example',
-                    'email': 'example@example.com'
+        self.client._auth_configs = auth.AuthConfig(
+            {
+                "auths": {
+                    "https://example.com": {
+                        "user": "example",
+                        "password": "example",
+                        "email": "example@example.com",
+                    }
                 }
             }
-        })
+        )
 
-        expected_params = {'t': None, 'q': False, 'dockerfile': None,
-                           'rm': False, 'nocache': False, 'pull': False,
-                           'forcerm': False,
-                           'remote': 'https://github.com/docker-library/mongo'}
+        expected_params = {
+            "t": None,
+            "q": False,
+            "dockerfile": None,
+            "rm": False,
+            "nocache": False,
+            "pull": False,
+            "forcerm": False,
+            "remote": "https://github.com/docker-library/mongo",
+        }
         expected_headers = {
-            'X-Registry-Config': auth.encode_header(
-                self.client._auth_configs.auths
-            )
+            "X-Registry-Config": auth.encode_header(self.client._auth_configs.auths)
         }
 
-        self.client.build(path='https://github.com/docker-library/mongo')
+        self.client.build(path="https://github.com/docker-library/mongo")
 
         fake_request.assert_called_with(
-            'POST',
+            "POST",
             f"{url_prefix}build",
             stream=True,
             data=None,
             headers=expected_headers,
             params=expected_params,
-            timeout=None
+            timeout=None,
         )
 
     def test_build_container_with_named_dockerfile(self):
-        self.client.build('.', dockerfile='nameddockerfile')
+        self.client.build(".", dockerfile="nameddockerfile")
+
+    def test_build_with_invalid_tag(self):
+        with pytest.raises(errors.DockerException):
+            self.client.build(".", tag="https://example.com")
 
     def test_build_container_with_container_limits(self):
-        self.client.build('.', container_limits={
-            'memory': 1024 * 1024,
-            'cpusetcpus': 1,
-            'cpushares': 1000,
-            'memswap': 1024 * 1024 * 8
-        })
+        self.client.build(
+            ".",
+            container_limits={
+                "memory": 1024 * 1024,
+                "cpusetcpus": 1,
+                "cpushares": 1000,
+                "memswap": 1024 * 1024 * 8,
+            },
+        )
 
     def test_build_container_invalid_container_limits(self):
         with pytest.raises(docker.errors.DockerException):
-            self.client.build('.', container_limits={
-                'foo': 'bar'
-            })
+            self.client.build(".", container_limits={"foo": "bar"})
 
     def test_set_auth_headers_with_empty_dict_and_auth_configs(self):
-        self.client._auth_configs = auth.AuthConfig({
-            'auths': {
-                'https://example.com': {
-                    'user': 'example',
-                    'password': 'example',
-                    'email': 'example@example.com'
+        self.client._auth_configs = auth.AuthConfig(
+            {
+                "auths": {
+                    "https://example.com": {
+                        "user": "example",
+                        "password": "example",
+                        "email": "example@example.com",
+                    }
                 }
             }
-        })
+        )
 
         headers = {}
         expected_headers = {
-            'X-Registry-Config': auth.encode_header(
-                self.client._auth_configs.auths
-            )
+            "X-Registry-Config": auth.encode_header(self.client._auth_configs.auths)
         }
 
         self.client._set_auth_headers(headers)
         assert headers == expected_headers
 
     def test_set_auth_headers_with_dict_and_auth_configs(self):
-        self.client._auth_configs = auth.AuthConfig({
-            'auths': {
-                'https://example.com': {
-                    'user': 'example',
-                    'password': 'example',
-                    'email': 'example@example.com'
+        self.client._auth_configs = auth.AuthConfig(
+            {
+                "auths": {
+                    "https://example.com": {
+                        "user": "example",
+                        "password": "example",
+                        "email": "example@example.com",
+                    }
                 }
             }
-        })
+        )
 
-        headers = {'foo': 'bar'}
+        headers = {"foo": "bar"}
         expected_headers = {
-            'X-Registry-Config': auth.encode_header(
-                self.client._auth_configs.auths
-            ),
-            'foo': 'bar'
+            "X-Registry-Config": auth.encode_header(self.client._auth_configs.auths),
+            "foo": "bar",
         }
 
         self.client._set_auth_headers(headers)
         assert headers == expected_headers
 
     def test_set_auth_headers_with_dict_and_no_auth_configs(self):
-        headers = {'foo': 'bar'}
-        expected_headers = {
-            'foo': 'bar'
-        }
+        headers = {"foo": "bar"}
+        expected_headers = {"foo": "bar"}
 
         self.client._set_auth_headers(headers)
         assert headers == expected_headers
 
     @pytest.mark.skipif(
-        not docker.constants.IS_WINDOWS_PLATFORM,
-        reason='Windows-specific syntax')
+        not docker.constants.IS_WINDOWS_PLATFORM, reason="Windows-specific syntax"
+    )
     def test_process_dockerfile_win_longpath_prefix(self):
         dirs = [
-            'foo', 'foo/bar', 'baz',
+            "foo",
+            "foo/bar",
+            "baz",
         ]
 
         files = [
-            'Dockerfile', 'foo/Dockerfile.foo', 'foo/bar/Dockerfile.bar',
-            'baz/Dockerfile.baz',
+            "Dockerfile",
+            "foo/Dockerfile.foo",
+            "foo/bar/Dockerfile.bar",
+            "baz/Dockerfile.baz",
         ]
 
         base = make_tree(dirs, files)
@@ -186,40 +211,42 @@ def pre(path):
             return docker.constants.WINDOWS_LONGPATH_PREFIX + path
 
         assert process_dockerfile(None, pre(base)) == (None, None)
-        assert process_dockerfile('Dockerfile', pre(base)) == (
-            'Dockerfile', None
+        assert process_dockerfile("Dockerfile", pre(base)) == ("Dockerfile", None)
+        assert process_dockerfile("foo/Dockerfile.foo", pre(base)) == (
+            "foo/Dockerfile.foo",
+            None,
         )
-        assert process_dockerfile('foo/Dockerfile.foo', pre(base)) == (
-            'foo/Dockerfile.foo', None
+        assert process_dockerfile("../Dockerfile", pre(f"{base}\\foo"))[1] is not None
+        assert process_dockerfile("../baz/Dockerfile.baz", pre(f"{base}/baz")) == (
+            "../baz/Dockerfile.baz",
+            None,
         )
-        assert process_dockerfile(
-            '../Dockerfile', pre(f"{base}\\foo")
-        )[1] is not None
-        assert process_dockerfile(
-            '../baz/Dockerfile.baz', pre(f"{base}/baz")
-        ) == ('../baz/Dockerfile.baz', None)
 
     def test_process_dockerfile(self):
         dirs = [
-            'foo', 'foo/bar', 'baz',
+            "foo",
+            "foo/bar",
+            "baz",
         ]
 
         files = [
-            'Dockerfile', 'foo/Dockerfile.foo', 'foo/bar/Dockerfile.bar',
-            'baz/Dockerfile.baz',
+            "Dockerfile",
+            "foo/Dockerfile.foo",
+            "foo/bar/Dockerfile.bar",
+            "baz/Dockerfile.baz",
         ]
 
         base = make_tree(dirs, files)
         self.addCleanup(shutil.rmtree, base)
 
         assert process_dockerfile(None, base) == (None, None)
-        assert process_dockerfile('Dockerfile', base) == ('Dockerfile', None)
-        assert process_dockerfile('foo/Dockerfile.foo', base) == (
-            'foo/Dockerfile.foo', None
+        assert process_dockerfile("Dockerfile", base) == ("Dockerfile", None)
+        assert process_dockerfile("foo/Dockerfile.foo", base) == (
+            "foo/Dockerfile.foo",
+            None,
         )
-        assert process_dockerfile(
-            '../Dockerfile', f"{base}/foo"
-        )[1] is not None
-        assert process_dockerfile('../baz/Dockerfile.baz', f"{base}/baz") == (
-            '../baz/Dockerfile.baz', None
+        assert process_dockerfile("../Dockerfile", f"{base}/foo")[1] is not None
+        assert process_dockerfile("../baz/Dockerfile.baz", f"{base}/baz") == (
+            "../baz/Dockerfile.baz",
+            None,
         )