Adding member is through 502 error

I am using the self-hosted taiga version. It was running well, but since last week, we have been unable to add any members to the project. On attempting to invite a member, it throws a 502 error.

image

Headers
Request URL: https:///api/v1/memberships/bulk_create
Request Method: POST
Status Code: 502 Bad Gateway
Remote Address: ...:*
Referrer Policy: strict-origin-when-cross-origin

Payload:
{“project_id”:103,“bulk_memberships”:[{“role_id”:674,“username”:“moiz”}]}

How can we troubleshoot? From where we can solve this issue?

Hi @qasimshabbir, welcome to our community.
May I assume you’ve followed the 30min taiga setup? If your answer is positive, I’ve seen there is no domain in the request URL, this could be the problem.

Actually, I hid the domain URLs for security purposes.
Request URL: https://mydomain/api/v1/memberships/bulk_create

Hi @qasimshabbir,

You can run $ docker compose logs -f and repeat the same operation to review the logs from the different containers. Can you post these traces?

Do you have an Nginx proxy passing the requests to your docker installation instance? If so, what’s your configuration? It can be a 443 problem as the request secured (https).

Is it the only operation that gives you that problem? This request may attempt to send an email to the new users, so it can also be a wrong SMTP configuration.

Thanks

Thank you again for the response, you are correct it’s an SMTP issue. The nignix got timed out on sending emails. It’s sending emails correctly on all other notifications, but on user creation, it was throwing an SSL error.

For an interim fix, I have disabled the email notification. Now, adding members is working.

Here is the stacktrace

OSError at /api/v1/memberships/bulk_create
[Errno 101] Network is unreachable
Request Method:	POST
Request URL:	https://taiga.q-sols.com:1025/api/v1/memberships/bulk_create
Django Version:	3.2.18
Exception Type:	OSError
Exception Value:	
[Errno 101] Network is unreachable
Exception Location:	/usr/lib/python3.9/socket.py, line 831, in create_connection
Python Executable:	/home/taiga/taiga-back/.venv/bin/python3
Python Version:	3.9.5
Python Path:	
['/home/taiga/taiga-back',
 '/home/taiga/taiga-back',
 '/home/taiga/taiga-back/.venv/bin',
 '/usr/lib/python39.zip',
 '/usr/lib/python3.9',
 '/usr/lib/python3.9/lib-dynload',
 '/home/taiga/taiga-back/.venv/lib/python3.9/site-packages']
Server time:	Thu, 02 Nov 2023 08:52:18 +0000
Traceback Switch to copy-and-paste view
/home/taiga/taiga-back/.venv/lib/python3.9/site-packages/django/core/handlers/exception.py, line 47, in inner
                response = await sync_to_async(response_for_exception, thread_sensitive=False)(request, exc)
            return response
        return inner
    else:
        @wraps(get_response)
        def inner(request):
            try:
                response = get_response(request) …
            except Exception as exc:
                response = response_for_exception(request, exc)
            return response
        return inner
▶ Local vars
Variable	Value
exc	
OSError(101, 'Network is unreachable')
get_response	
<bound method BaseHandler._get_response of <django.core.handlers.wsgi.WSGIHandler object at 0x7f2057a79760>>
request	
<WSGIRequest: POST '/api/v1/memberships/bulk_create'>
/home/taiga/taiga-back/.venv/lib/python3.9/site-packages/django/core/handlers/base.py, line 181, in _get_response
        if response is None:
            wrapped_callback = self.make_view_atomic(callback)
            # If it is an asynchronous view, run it in a subthread.
            if asyncio.iscoroutinefunction(wrapped_callback):
                wrapped_callback = async_to_sync(wrapped_callback)
            try:
                response = wrapped_callback(request, *callback_args, **callback_kwargs) …
            except Exception as e:
                response = self.process_exception_by_middleware(e, request)
                if response is None:
                    raise
        # Complain if the view returned None (a common error).
▶ Local vars
Variable	Value
callback	
<function MembershipViewSet at 0x7f2057c20c10>
callback_args	
()
callback_kwargs	
{}
middleware_method	
<bound method SentryMiddleware.process_view of <raven.contrib.django.middleware.SentryMiddleware object at 0x7f2057a9d700>>
request	
<WSGIRequest: POST '/api/v1/memberships/bulk_create'>
response	
None
self	
<django.core.handlers.wsgi.WSGIHandler object at 0x7f2057a79760>
wrapped_callback	
<function MembershipViewSet at 0x7f2057c20c10>
/home/taiga/taiga-back/taiga/base/api/viewsets.py, line 95, in view
                setattr(self, method, handler)
            # Patch this in as it's otherwise only present from 1.5 onwards
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            # And continue as usual
            return self.dispatch(request, *args, **kwargs) …
        # take name and docstring from class
        update_wrapper(view, cls, updated=())
        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
▶ Local vars
Variable	Value
action	
'bulk_create'
actions	
{'post': 'bulk_create'}
args	
()
cls	
<class 'taiga.projects.api.MembershipViewSet'>
handler	
<bound method MembershipViewSet.bulk_create of <taiga.projects.api.MembershipViewSet object at 0x7f20578f5b20>>
initkwargs	
{}
kwargs	
{}
method	
'post'
request	
<WSGIRequest: POST '/api/v1/memberships/bulk_create'>
self	
<taiga.projects.api.MembershipViewSet object at 0x7f20578f5b20>
/home/taiga/taiga-back/.venv/lib/python3.9/site-packages/django/views/decorators/csrf.py, line 54, in wrapped_view
def csrf_exempt(view_func):
    """Mark a view function as being exempt from the CSRF view protection."""
    # view_func.csrf_exempt = True would also work, but decorators are nicer
    # if they don't have side effects, so return a new function.
    def wrapped_view(*args, **kwargs):
        return view_func(*args, **kwargs) …
    wrapped_view.csrf_exempt = True
    return wraps(view_func)(wrapped_view)
▶ Local vars
Variable	Value
args	
(<taiga.projects.api.MembershipViewSet object at 0x7f20578f5b20>,
 <WSGIRequest: POST '/api/v1/memberships/bulk_create'>)
kwargs	
{}
view_func	
<function APIView.dispatch at 0x7f205de524c0>
/home/taiga/taiga-back/taiga/base/api/views.py, line 449, in dispatch
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed
            response = handler(request, *args, **kwargs)
        except Exception as exc:
            response = self.handle_exception(exc) …
        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response
    def options(self, request, *args, **kwargs):
        """
▶ Local vars
Variable	Value
args	
()
handler	
<bound method MembershipViewSet.bulk_create of <taiga.projects.api.MembershipViewSet object at 0x7f20578f5b20>>
kwargs	
{}
request	
<taiga.base.api.request.Request object at 0x7f20578f58b0>
self	
<taiga.projects.api.MembershipViewSet object at 0x7f20578f5b20>
/home/taiga/taiga-back/taiga/base/api/views.py, line 447, in dispatch
            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed
            response = handler(request, *args, **kwargs) …
        except Exception as exc:
            response = self.handle_exception(exc)
        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response
▶ Local vars
Variable	Value
args	
()
handler	
<bound method MembershipViewSet.bulk_create of <taiga.projects.api.MembershipViewSet object at 0x7f20578f5b20>>
kwargs	
{}
request	
<taiga.base.api.request.Request object at 0x7f20578f58b0>
self	
<taiga.projects.api.MembershipViewSet object at 0x7f20578f5b20>
/home/taiga/taiga-back/taiga/projects/api.py, line 1066, in bulk_create
        try:
            with advisory_lock("membership-creation-{}".format(project.id)):
                members = services.get_members_from_bulk(bulk_memberships,
                                                         project=project,
                                                         invited_by=request.user,
                                                         invitation_extra_text=invitation_extra_text)
                self._check_if_new_members_can_be_created(project, members)
                services.create_members_in_bulk(members, callback=self.post_save) …
                signal_members_added.send(sender=self.__class__,
                                          user=self.request.user,
                                          project=project,
                                          new_members=members)
        except exc.ValidationError as err:
            return response.BadRequest(err.message_dict)
▶ Local vars
Variable	Value
bulk_memberships	
[OrderedDictWithMetadata([('username', 'ahsandevops'), ('role_id', 664)])]
context	
{'request': <taiga.base.api.request.Request object at 0x7f20578f58b0>}
data	
OrderedDictWithMetadata([('project_id', 102),
                         ('bulk_memberships',
                          [OrderedDictWithMetadata([('username', 'ahsandevops'),
                                                    ('role_id', 664)])]),
                         ('invitation_extra_text', None)])
invitation_extra_text	
None
kwargs	
{}
members	
[<Membership: Membership object (1041)>]
project	
<Project 102>
request	
<taiga.base.api.request.Request object at 0x7f20578f58b0>
self	
<taiga.projects.api.MembershipViewSet object at 0x7f20578f5b20>
validator	
<taiga.projects.validators.MembersBulkValidator object at 0x7f20578f5c40>
/home/taiga/taiga-back/taiga/projects/services/members.py, line 66, in create_members_in_bulk
    """Create members from `bulk_data`.
    :param members: List of dicts `{"project_id": <>, "role_id": <>, "username": <>}`.
    :param callback: Callback to execute after each task save.
    :param additional_fields: Additional fields when instantiating each task.
    """
    db.save_in_bulk(members, callback, precall) …
def remove_user_from_project(user, project):
    models.Membership.objects.get(project=project, user=user).delete()
▶ Local vars
Variable	Value
callback	
<bound method MembershipViewSet.post_save of <taiga.projects.api.MembershipViewSet object at 0x7f20578f5b20>>
members	
[<Membership: Membership object (1041)>]
precall	
None
/usr/lib/python3.9/contextlib.py, line 79, in inner
        """
        return self
    def __call__(self, func):
        @wraps(func)
        def inner(*args, **kwds):
            with self._recreate_cm():
                return func(*args, **kwds) …
        return inner
class _GeneratorContextManagerBase:
    """Shared functionality for @contextmanager and @asynccontextmanager."""
▶ Local vars
Variable	Value
args	
([<Membership: Membership object (1041)>],
 <bound method MembershipViewSet.post_save of <taiga.projects.api.MembershipViewSet object at 0x7f20578f5b20>>,
 None)
func	
<function save_in_bulk at 0x7f205d42e430>
kwds	
{}
self	
<django.db.transaction.Atomic object at 0x7f205d430310>
/home/taiga/taiga-back/taiga/base/utils/db.py, line 88, in save_in_bulk
    for instance in instances:
        created = False
        if instance.pk is None:
            created = True
        precall(instance)
        instance.save(**save_options)
        callback(instance, created=created) …
    return ret
@transaction.atomic
def update_in_bulk(instances, list_of_new_values, callback=None, precall=None):
▶ Local vars
Variable	Value
callback	
<bound method MembershipViewSet.post_save of <taiga.projects.api.MembershipViewSet object at 0x7f20578f5b20>>
created	
True
instance	
<Membership: Membership object (1041)>
instances	
[<Membership: Membership object (1041)>]
precall	
<function noop at 0x7f205d42e1f0>
ret	
[]
save_options	
{}
/home/taiga/taiga-back/taiga/projects/api.py, line 1158, in post_save
    def post_save(self, object, created=False):
        super().post_save(object, created=created)
        if not created:
            return
        # Send email only if a new membership is created
        services.send_invitation(invitation=object) …
class InvitationViewSet(ModelListViewSet):
    """
    Only used by front for get invitation by it token.
    """
▶ Local vars
Variable	Value
__class__	
<class 'taiga.projects.api.MembershipViewSet'>
created	
True
object	
<Membership: Membership object (1041)>
self	
<taiga.projects.api.MembershipViewSet object at 0x7f20578f5b20>
/home/taiga/taiga-back/taiga/projects/services/invitations.py, line 29, in send_invitation
    if invitation.user:
        template = mail_builder.membership_notification
        email = template(invitation.user, {"membership": invitation})
    else:
        template = mail_builder.membership_invitation
        email = template(invitation.email, {"membership": invitation})
    email.send() …
def find_invited_user(email, default=None):
    """Check if the invited user is already a registered.
    :param email: some user email
▶ Local vars
Variable	Value
email	
<django.core.mail.message.EmailMultiAlternatives object at 0x7f2057961e80>
invitation	
<Membership: Membership object (1041)>
template	
<function MagicMailBuilder.__getattr__.<locals>._dynamic_email_generator at 0x7f2057953f70>
/home/taiga/taiga-back/.venv/lib/python3.9/site-packages/django/core/mail/message.py, line 284, in send
    def send(self, fail_silently=False):
        """Send the email message."""
        if not self.recipients():
            # Don't bother creating the network connection if there's nobody to
            # send to.
            return 0
        return self.get_connection(fail_silently).send_messages([self]) …
    def attach(self, filename=None, content=None, mimetype=None):
        """
        Attach a file with the given filename and content. The filename can
        be omitted and the mimetype is guessed, if not provided.
▶ Local vars
Variable	Value
fail_silently	
False
self	
<django.core.mail.message.EmailMultiAlternatives object at 0x7f2057961e80>
/home/taiga/taiga-back/.venv/lib/python3.9/site-packages/django/core/mail/backends/smtp.py, line 102, in send_messages
        """
        Send one or more EmailMessage objects and return the number of email
        messages sent.
        """
        if not email_messages:
            return 0
        with self._lock:
            new_conn_created = self.open() …
            if not self.connection or new_conn_created is None:
                # We failed silently on open().
                # Trying to send would be pointless.
                return 0
            num_sent = 0
            for message in email_messages:
▶ Local vars
Variable	Value
email_messages	
[<django.core.mail.message.EmailMultiAlternatives object at 0x7f2057961e80>]
self	
<django.core.mail.backends.smtp.EmailBackend object at 0x7f2057961df0>
/home/taiga/taiga-back/.venv/lib/python3.9/site-packages/django/core/mail/backends/smtp.py, line 62, in open
            connection_params['timeout'] = self.timeout
        if self.use_ssl:
            connection_params.update({
                'keyfile': self.ssl_keyfile,
                'certfile': self.ssl_certfile,
            })
        try:
            self.connection = self.connection_class(self.host, self.port, **connection_params) …
            # TLS/SSL are mutually exclusive, so only attempt TLS over
            # non-secure connections.
            if not self.use_ssl and self.use_tls:
                self.connection.starttls(keyfile=self.ssl_keyfile, certfile=self.ssl_certfile)
            if self.username and self.password:
▶ Local vars
Variable	Value
connection_params	
{'local_hostname': 'ubuntu-server'}
self	
<django.core.mail.backends.smtp.EmailBackend object at 0x7f2057961df0>
/usr/lib/python3.9/smtplib.py, line 255, in __init__
        self.timeout = timeout
        self.esmtp_features = {}
        self.command_encoding = 'ascii'
        self.source_address = source_address
        self._auth_challenge_count = 0
        if host:
            (code, msg) = self.connect(host, port) …
            if code != 220:
                self.close()
                raise SMTPConnectError(code, msg)
        if local_hostname is not None:
            self.local_hostname = local_hostname
        else:
▶ Local vars
Variable	Value
host	
'smtp.gmail.com'
local_hostname	
'ubuntu-server'
port	
587
self	
<smtplib.SMTP object at 0x7f205771a8e0>
source_address	
None
timeout	
<object object at 0x7f2060c5e8a0>
/usr/lib/python3.9/smtplib.py, line 341, in connect
                try:
                    port = int(port)
                except ValueError:
                    raise OSError("nonnumeric port")
        if not port:
            port = self.default_port
        sys.audit("smtplib.connect", self, host, port)
        self.sock = self._get_socket(host, port, self.timeout) …
        self.file = None
        (code, msg) = self.getreply()
        if self.debuglevel > 0:
            self._print_debug('connect:', repr(msg))
        return (code, msg)
▶ Local vars
Variable	Value
host	
'smtp.gmail.com'
port	
587
self	
<smtplib.SMTP object at 0x7f205771a8e0>
source_address	
None
/usr/lib/python3.9/smtplib.py, line 312, in _get_socket
    def _get_socket(self, host, port, timeout):
        # This makes it simpler for SMTP_SSL to use the SMTP connect code
        # and just alter the socket connection bit.
        if timeout is not None and not timeout:
            raise ValueError('Non-blocking socket (timeout=0) is not supported')
        if self.debuglevel > 0:
            self._print_debug('connect: to', (host, port), self.source_address)
        return socket.create_connection((host, port), timeout, …
                                        self.source_address)
    def connect(self, host='localhost', port=0, source_address=None):
        """Connect to a host on a given port.
        If the hostname ends with a colon (`:') followed by a number, and
▶ Local vars
Variable	Value
host	
'smtp.gmail.com'
port	
587
self	
<smtplib.SMTP object at 0x7f205771a8e0>
timeout	
<object object at 0x7f2060c5e8a0>
/usr/lib/python3.9/socket.py, line 843, in create_connection
        except error as _:
            err = _
            if sock is not None:
                sock.close()
    if err is not None:
        try:
            raise err …
        finally:
            # Break explicitly a reference cycle
            err = None
    else:
        raise error("getaddrinfo returns an empty list")
▶ Local vars
Variable	Value
address	
('smtp.gmail.com', 587)
af	
<AddressFamily.AF_INET6: 10>
canonname	
''
err	
None
host	
'smtp.gmail.com'
port	
587
proto	
6
res	
(<AddressFamily.AF_INET6: 10>,
 <SocketKind.SOCK_STREAM: 1>,
 6,
 '',
 ('2a00:1450:400c:c00::6d', 587, 0, 0))
sa	
('2a00:1450:400c:c00::6d', 587, 0, 0)
sock	
<socket.socket [closed] fd=-1, family=AddressFamily.AF_INET6, type=SocketKind.SOCK_STREAM, proto=6>
socktype	
<SocketKind.SOCK_STREAM: 1>
source_address	
None
timeout	
<object object at 0x7f2060c5e8a0>
/usr/lib/python3.9/socket.py, line 831, in create_connection
        sock = None
        try:
            sock = socket(af, socktype, proto)
            if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
                sock.settimeout(timeout)
            if source_address:
                sock.bind(source_address)
            sock.connect(sa) …
            # Break explicitly a reference cycle
            err = None
            return sock
        except error as _:
            err = _
▶ Local vars
Variable	Value
address	
('smtp.gmail.com', 587)
af	
<AddressFamily.AF_INET6: 10>
canonname	
''
err	
None
host	
'smtp.gmail.com'
port	
587
proto	
6
res	
(<AddressFamily.AF_INET6: 10>,
 <SocketKind.SOCK_STREAM: 1>,
 6,
 '',
 ('2a00:1450:400c:c00::6d', 587, 0, 0))
sa	
('2a00:1450:400c:c00::6d', 587, 0, 0)
sock	
<socket.socket [closed] fd=-1, family=AddressFamily.AF_INET6, type=SocketKind.SOCK_STREAM, proto=6>
socktype	
<SocketKind.SOCK_STREAM: 1>
source_address	
None
timeout	
<object object at 0x7f2060c5e8a0>
Environment:


Request Method: POST
Request URL: https://taiga.q-sols.com:1025/api/v1/memberships/bulk_create

Django Version: 3.2.18
Python Version: 3.9.5

It seems it can be a bad configuration on your EMAIL variables in your .env file, so I’d try changing any combination of the variables.

According to your logs you’re using GMAIL as the SMTP server, and although I’m not 100% sure about it, the following config may work:

EMAIL_BACKEND=smtp
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_HOST_USER=XXX
EMAIL_HOST_PASSWORD=XXX
EMAIL_DEFAULT_FROM=changeme@example.com

# EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusive (only set one of those to True)
EMAIL_USE_TLS=True  # use TLS (secure) connection with the SMTP server
EMAIL_USE_SSL=False  # use implicit TLS (secure) connection with the SMTP server

If it doesn’t work, you can try with an App password instead of your normal Google account login, or research more about your OSError(101, ‘Network is unreachable’ problem in Django.

Perhaps other member is using GMAIL and can help you more. Tell us if you finally achieve a solution.