0

I have a working environment with:

  • django==1.10
  • django-rest-framework==3.5.3
  • djangorestframework-jsonapi==2.1.1
  • channels (latest)
  • daphne (latest) instead of gunicorn.

I'm using nginx as a proxy server above daphne, inside a docker environment.

I'm building a separate angular 2 SPA that connects to the above backend and I'm using django-cors-headers==2.0.2 to allow connections from that web app.

It works with: USE_I18N = False

It works fine when I set Django's USE_I18N = False. When trying to authenticate against the backend, I send a POST request equivalent to:

curl -H "Content-Type: application/vnd.api+json" -X POST -d '{"data": {"type": "obtainJSONWebTokens", "attributes": {"email":"admin@email.com", "password":"password"}}}' http://localhost/api/auth/login/ --verbose

Output from curl:

*   Trying ::1...
* Connected to localhost (::1) port 80 (#0)
> POST /api/auth/login/ HTTP/1.1
> Host: localhost
> User-Agent: curl/7.49.0
> Accept: */*
> Content-Type: application/vnd.api+json
> Content-Length: 107
>
* upload completely sent off: 107 out of 107 bytes
< HTTP/1.1 200 OK
< Server: nginx/1.11.9
< Date: Mon, 20 Mar 2017 13:00:47 GMT
< Content-Type: application/vnd.api+json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Allow: POST, OPTIONS
< X-Frame-Options: SAMEORIGIN
< Content-Language: en
< Vary: Accept, Accept-Language, Cookie
<
{"data":{"token":"<token>"}}
* Connection #0 to host localhost left intact

I receive the JWT Token that I am supposed to receive. All works fine.

It fails with: USE_I18N = True

However, the same connection fails when USE_I18N = True.

Output from curl:

*   Trying ::1...
* Connected to localhost (::1) port 80 (#0)
> POST /api/auth/login/ HTTP/1.1
> Host: localhost
> User-Agent: curl/7.49.0
> Accept: */*
> Content-Type: application/vnd.api+json
> Content-Length: 107

* upload completely sent off: 107 out of 107 bytes
< HTTP/1.1 302 Found
< Server: nginx/1.11.9
< Date: Mon, 20 Mar 2017 12:53:49 GMT
< Content-Type: text/html; charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Location: /en/api/auth/login/
< Vary: Cookie
<
* Connection #0 to host localhost left intact

The returned error on the client side is:

XMLHttpRequest cannot load http://localhost/api/auth/login/. Redirect from 'http://localhost/api/auth/login/' to 'http://localhost/en/api/auth/login/' has been blocked by CORS policy: Request requires preflight, which is disallowed to follow cross-origin redirect.

Relevant settings:

INSTALLED_APPS += (
    'corsheaders',
)

if DEBUG is True:
    CORS_ORIGIN_ALLOW_ALL = True

MIDDLEWARE_CLASSES = (
    'corsheaders.middleware.CorsMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.admindocs.middleware.XViewMiddleware',
)

It would seem it's not the client request that fails but the redirection from 'http://localhost/api/auth/login/' to 'http://localhost/en/api/auth/login/', where Django adds the 'en' to the URL.

Can someone shed any light into this?

I've searched for django-cors-headers related issues but none is specific to this apparent incompatibility with I18N. The library works fine without I18N, just not with it on.

EDIT 2017-03-21

Given the limitations stated in the accepted answer, I opted by simply avoiding Django's language URL redirections. While using USE_I18N = True, I completely avoided i18n_patterns in the root URLconf.

In fact, Django Rest Framework states this is a best practice for API clients:

If you want to allow per-request language preferences you'll need to include django.middleware.locale.LocaleMiddleware in your MIDDLEWARE_CLASSES setting.

You can find more information on how the language preference is determined in the Django documentation. For reference, the method is:

  • First, it looks for the language prefix in the requested URL.
  • Failing that, it looks for the LANGUAGE_SESSION_KEY key in the current user’s session.
  • Failing that, it looks for a cookie.
  • Failing that, it looks at the Accept-Language HTTP header.
  • Failing that, it uses the global LANGUAGE_CODE setting.

For API clients the most appropriate of these will typically be to use the Accept-Language header; Sessions and cookies will not be available unless using session authentication, and generally better practice to prefer an Accept-Language header for API clients rather than using language URL prefixes.

So, I kept the above settings the same but changed the following in the root URLconf:

urlpatterns += i18n_patterns(
    url(_(r'^api/$'), SwaggerSchemaView.as_view(), name='api'),
    url(_(r'^api/account/'), include(account_patterns, namespace='account')),
    url(_(r'^api/auth/'), include(auth_patterns, namespace='auth')),
    url(_(r'^api/'), include('apps.party.api.urls', namespace='parties')),
    url(_(r'^api/'), include('apps.i18n.api.urls', namespace='i18n')),
    url(_(r'^api-auth/'), include('rest_framework.urls', namespace='rest_framework')),
    url(_(r'^admin/'), include(admin_patterns)),
    url(_(r'^docs/'), include('apps.docs.urls'))
)

to

urlpatterns += ([
    url(_(r'^api/$'), SwaggerSchemaView.as_view(), name='api'),
    url(_(r'^api/account/'), include(account_patterns, namespace='account')),
    url(_(r'^api/auth/'), include(auth_patterns, namespace='auth')),
    url(_(r'^api/'), include('apps.party.api.urls', namespace='parties')),
    url(_(r'^api/'), include('apps.i18n.api.urls', namespace='i18n')),
    url(_(r'^api-auth/'), include('rest_framework.urls',     namespace='rest_framework')),
    url(_(r'^admin/'), include(admin_patterns)),
    url(_(r'^docs/'), include('apps.docs.urls'))]
)

So, now, doing:

curl -H "Content-Type: application/vnd.api+json" -H "Accept-Language: pt" -X POST -d '{"data": {"type": "obtainJSONWebTokens", "attributes": {"email":"admin@email.com", "password":"password"}}}' http://localhost:8000/api/auth/login/ --verbose

returns the expected response in the requested language (please notice the inclusion of "Accept-Language: pt" in the request above):

*   Trying ::1...
* Connected to localhost (::1) port 8000 (#0)
> POST /api/auth/login/ HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.49.0
> Accept: */*
> Content-Type: application/vnd.api+json
> Accept-Language: pt
> Content-Length: 107
>
* upload completely sent off: 107 out of 107 bytes
< HTTP/1.1 200 OK
< Transfer-Encoding: chunked
< Allow: POST, OPTIONS
< X-Frame-Options: SAMEORIGIN
< Vary: Accept, Accept-Language, Cookie
< Content-Language: pt
< Content-Type: application/vnd.api+json
<
{"data":    {"token":"<token>"}}
*     Connection #0 to host localhost left intact
lfreire
  • 103
  • 9

2 Answers2

0

In essence, you're running into a bug in an older version of the CORS standard.

The original standard basically made it impossible to do local redirects when using prelight requests. See this question on the subject, along with this bug report on the Fetch standard.

In your case this happens with USE_I18N = True because that setting is what triggers the redirects.

Hopefully the fix will soon be implemented by the browsers. (According to the latest report on the Fetch bug it already works in Edge.) In the meantime, this answer suggests some workarounds.

Community
  • 1
  • 1
Kevin Christopher Henry
  • 46,175
  • 7
  • 116
  • 102
  • I accepted this answer. Given the limitations stated in the explanations and links provided, I opted by simply avoiding Django's language redirections. While using `USE_I18N = True`, I completely avoided `i18n_patterns`. I've edited the question with more details. – lfreire Mar 21 '17 at 17:49
0

I had the same problem since I did not use the i18n_patterns for all my URLs and one of the URLs that was not inside i18n_patterns returned a 404 response. I solved it by overwriting the Middleware LocaleMiddleware that imports Django by default.

class CustomLocaleMiddleware(LocaleMiddleware): 
  def current_urlpattern_is_locale(self, path):
      try:
          resolver = get_resolver(None).resolve(path)
      except Resolver404:
        return self.is_language_prefix_patterns_used()
      return isinstance(resolver, LocaleRegexURLResolver)

  def process_response(self, request, response):
    language = translation.get_language()
    language_from_path = translation.get_language_from_path(request.path_info)
    if (response.status_code == 404 and not language_from_path
            and self.current_urlpattern_is_locale(request.path)):
        urlconf = getattr(request, 'urlconf', None)
        language_path = '/%s%s' % (language, request.path_info)
        path_valid = is_valid_path(language_path, urlconf)
        if (not path_valid and settings.APPEND_SLASH
                and not language_path.endswith('/')):
            path_valid = is_valid_path("%s/" % language_path, urlconf)

        if path_valid:
            script_prefix = get_script_prefix()
            language_url = "%s://%s%s" % (
                request.scheme,
                request.get_host(),
                # insert language after the script prefix and before the
                # rest of the URL
                request.get_full_path().replace(
                    script_prefix,
                    '%s%s/' % (script_prefix, language),
                    1
                )
            )
            return self.response_redirect_class(language_url)

    if not (self.is_language_prefix_patterns_used()
            and language_from_path):
        patch_vary_headers(response, ('Accept-Language',))
    if 'Content-Language' not in response:
        response['Content-Language'] = language
    return response
Nathan Tuggy
  • 2,237
  • 27
  • 30
  • 38