django-cors-headers не работает, когда включен i18n

У меня есть рабочая среда с:

  • django==1.10
  • django-rest-framework==3.5.3
  • djangorestframework-jsonapi==2.1.1
  • channels (самый последний)
  • daphne (последний) вместо gunicorn,

я использую nginx как прокси сервер выше daphneвнутри докера

Я строю отдельный angular 2 SPA, который подключается к вышеупомянутому бэкэнду, и я использую django-cors-headers==2.0.2 разрешить подключения из этого веб-приложения.

Работает с: USE_I18N = False

Он отлично работает, когда я установил Django USE_I18N = False, При попытке аутентификации в бэкэнде я отправляю запрос POST, эквивалентный следующему:

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

Вывод из 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

Я получаю токен JWT, который должен получить. Все работает отлично.

Сбой с: USE_I18N = True

Тем не менее, то же соединение не удается, когда USE_I18N = True,

Вывод из 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

Возвращенная ошибка на стороне клиента:

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.

Соответствующие настройки:

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',
)

Казалось бы, это не запрос клиента, но сбой, а перенаправление с " http://localhost/api/auth/login/" на " http://localhost/en/api/auth/login/", где Django добавляет 'en' к URL.

Может кто-то пролить свет на это?

Я искал django-cors-headers связанные с этим проблемы, но ни одна из них не относится к этой явной несовместимости с I18N. Библиотека прекрасно работает без I18N, только не с включенным.

РЕДАКТИРОВАТЬ 2017-03-21

Учитывая ограничения, указанные в принятом ответе, я решил просто избегать перенаправления URL-адреса языка Django. При использовании USE_I18N = TrueЯ полностью избежал i18n_patterns в корневом URLconf.

Фактически, Django Rest Framework заявляет, что это лучшая практика для клиентов API:

Если вы хотите разрешить языковые предпочтения для каждого запроса, вам необходимо включить django.middleware.locale.LocaleMiddleware в вашем MIDDLEWARE_CLASSES установка.

Вы можете найти больше информации о том, как языковые предпочтения определены в документации Django. Для справки, метод:

  • Сначала он ищет языковой префикс в запрошенном URL.
  • В противном случае, он ищет LANGUAGE_SESSION_KEY введите текущий сеанс пользователя.
  • В противном случае он ищет печенье.
  • В противном случае это смотрит на Accept-Language HTTP заголовок.
  • В противном случае он использует глобальный LANGUAGE_CODE установка.

Для клиентов API наиболее подходящим из них обычно будет использование Accept-Language заголовок; Сеансы и файлы cookie не будут доступны, если не используется аутентификация сеанса, и, как правило, лучше использовать Accept-Language заголовок для клиентов API вместо использования языковых префиксов URL.

Итак, я сохранил вышеупомянутые настройки, но изменил в корне следующее 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'))
)

в

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'))]
)

Итак, сейчас занимаемся:

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

возвращает ожидаемый ответ на запрошенном языке (обратите внимание на включение "Accept-Language: pt" в запросе выше):

*   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

2 ответа

Решение

По сути, вы столкнулись с ошибкой в ​​более старой версии стандарта CORS.

Первоначальный стандарт в основном делал невозможным локальное перенаправление при использовании предварительных запросов. См. Этот вопрос по теме, а также отчет об ошибках в стандарте Fetch.

В вашем случае это происходит с USE_I18N = True потому что этот параметр вызывает перенаправления.

Надеюсь, это исправление скоро будет реализовано браузерами. (Согласно последнему отчету об ошибке Fetch, она уже работает в Edge.) Тем временем этот ответ предлагает некоторые обходные пути.

У меня была такая же проблема, так как я не использовал i18n_patterns для всех моих URL и одного из URL, который не был внутри i18n_patterns вернул 404 ответ. Я решил эту проблему, переписав Middleware LocaleMiddleware, который по умолчанию импортирует Django.

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
Другие вопросы по тегам