DRF 指的是 Django REST framework,后面都用 DRF 代替

昨天公司运维大佬在调用 API 接口获取内容(限制一页 20 条)时,发现访问下一页时报 404,当时临近下班就在个人环境查询下并没有复现,就想着明早来了再查问题,到家后仔细看了下代码才发现了问题,这才有了这篇文章,废话不多说开整。

环境

后端是用 DRF 构建的、前面用了 nginx 做了个代理。nginx 配置如下:

upstream dev {
    server 127.0.0.1:50050;
        keepalive 20;
}
upstream online {
    server 127.0.0.1:50051;
        keepalive 20;
}
server {
    listen      80;
    server_name x.x.x.x;
    location /dev/ {
        proxy_set_header   Host $http_host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_pass http://dev/;
        proxy_send_timeout 200;
        proxy_read_timeout 200;
        proxy_connect_timeout 200;
    }
    location /online/ {
        proxy_set_header   Host $http_host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_pass http://online/;
        proxy_send_timeout 200;
        proxy_read_timeout 200;
        proxy_connect_timeout 200;
    }
}

DRF 自定义分页如下:

from rest_framework.pagination import PageNumberPagination

from utils.custom_json_response_handler import JsonResponse
from rest_framework import status


#普通分页
class MyPageNumberPagination(PageNumberPagination):
    page_size = 20
    max_page_size = 200
    page_size_query_param = 'page_size'
    page_query_param = 'page'

    def get_paginated_response(self, data):
        return JsonResponse(data=data, code=201, msg="成功", status=status.HTTP_200_OK, next=self.get_next_link(),
                            previous=self.get_previous_link(), count=self.page.paginator.count)

现象

request包发起请求,请求地址为http://x.x.x.x/dev/cmdb/databasebackup/download/;返回数据为

{
  "code": 201,
  "msg": "成功",
  "data": [],
  "next": "http://x.x.x.x/cmdb/databasebackup/download/?page=2",
  "previous": null,
  "count": 1915
}

返回的下一页地址中少了 nginx location 路径,这导致访问导致下一页内容时报 404 错误,至于为啥我个人环境没有发现该问题,因为我直接用 runserver 启动的前面没有挂 nginx 代理。

分析

知道了下一页地址中缺少 location 路径,就找下为啥会缺少吧,先从get_next_link这个方法来看

def get_next_link(self):
        if not self.page.has_next():
            return None
        url = self.request.build_absolute_uri()
        page_number = self.page.next_page_number()
        return replace_query_param(url, self.page_query_param, page_number)

代码很简单 url 是通过build_absolute_uri方法获取到的、去看下这个

def build_absolute_uri(self, location=None):
        """
        Build an absolute URI from the location and the variables available in
        this request. If no ``location`` is specified, build the absolute URI
        using request.get_full_path(). If the location is absolute, convert it
        to an RFC 3987 compliant URI and return it. If location is relative or
        is scheme-relative (i.e., ``//example.com/``), urljoin() it to a base
        URL constructed from the request variables.
        """
        if location is None:
            # Make it an absolute url (but schemeless and domainless) for the
            # edge case that the path starts with '//'.
            location = '//%s' % self.get_full_path()
        bits = urlsplit(location)
        if not (bits.scheme and bits.netloc):
            # Handle the simple, most common case. If the location is absolute
            # and a scheme or host (netloc) isn't provided, skip an expensive
            # urljoin() as long as no path segments are '.' or '..'.
            if (bits.path.startswith('/') and not bits.scheme and not bits.netloc and
                    '/./' not in bits.path and '/../' not in bits.path):
                # If location starts with '//' but has no netloc, reuse the
                # schema and netloc from the current request. Strip the double
                # slashes and continue as if it wasn't specified.
                if location.startswith('//'):
                    location = location[2:]
                location = self._current_scheme_host + location
            else:
                # Join the constructed URL with the provided location, which
                # allows the provided location to apply query strings to the
                # base path.
                location = urljoin(self._current_scheme_host + self.path, location)
        return iri_to_uri(location)

通过上面代码可以得出通过_current_scheme_host方法可以得出http://xxxxx,通过self.path可以得到/cmdb/databasebackup/download/路径,而urljoin方法当第二个参数有值时会把第一个覆盖掉返回,因此我们只要重定义get_next_linkbuild_absolute_uri传入完整路径即可。

验证

自定义get_next_link如下:

class MyPageNumberPagination(PageNumberPagination):
    page_size = 20
    max_page_size = 200
    page_size_query_param = 'page_size'
    page_query_param = 'page'

    def get_next_link(self):
        if not self.page.has_next():
            return None
        # 解决前端location无法传给下一页
        url = self.request.build_absolute_uri(self.request.scheme + '://' + self.request.get_host() + '/'+ 'dev' + self.request.get_full_path())
        page_number = self.page.next_page_number()
        return replace_query_param(url, self.page_query_param, page_number)

直接写死 dev 路径,但是这样只能测试环境可以、线上环境和个人环境还是无法解决,正在苦恼时、突然灵光一现想到一个骚操作,可以通过 nginx 在 headers 里设置个 location 字段哈。开搞,修改后的 nginx 配置

upstream dev {
    server 127.0.0.1:50050;
        keepalive 20;
}
upstream online {
    server 127.0.0.1:50051;
        keepalive 20;
}
server {
    listen      80;
    server_name x.x.x.x;
    location /dev/ {
        proxy_set_header   Host $http_host;
        proxy_set_header   Location dev;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_pass http://dev/;
        proxy_send_timeout 200;
        proxy_read_timeout 200;
        proxy_connect_timeout 200;
    }
    location /online/ {
        proxy_set_header   Host $http_host;
        proxy_set_header   Location online;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_pass http://online/;
        proxy_send_timeout 200;
        proxy_read_timeout 200;
        proxy_connect_timeout 200;
    }
}

同时修改下get_next_link代码

def get_next_link(self):
        if not self.page.has_next():
            return None
        # 解决前端location无法传给下一页
        if self.request.headers.get("Location"):
            # url = self.request.build_absolute_uri(self.request.scheme + '://' + self.request.get_host() + '/'+ self.request.headers.get("Location") + self.request.get_full_path())
            # 也可以直接自定义url
            url = self.request.scheme + '://' + self.request.get_host() + '/' + self.request.headers.get("Location") + self.request.get_full_path()

        else:
            url = self.request.build_absolute_uri()
        page_number = self.page.next_page_number()
        return replace_query_param(url, self.page_query_param, page_number)

再次请求,下一页的地址已经完全正确了。后来问了下 nginx 大佬,发现一个更简单的解决办法只需要修改 nginx 配置即可,

upstream dev {
    server 127.0.0.1:50050;
        keepalive 20;
}
upstream online {
    server 127.0.0.1:50051;
        keepalive 20;
}
server {
    listen      80;
    server_name dev.xxxxx.com;

    location / {
        proxy_set_header   Host $http_host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_pass http://dev/;
        proxy_send_timeout 200;
        proxy_read_timeout 200;
        proxy_connect_timeout 200;
    }

}

server {
    listen      80;
    server_name online.xxxxx.com;
    location / {
        proxy_set_header   Host $http_host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_pass http://online/;
        proxy_send_timeout 200;
        proxy_read_timeout 200;
        proxy_connect_timeout 200;
    }

}

通过域名做反代,后端无需做修改即可。

Last Updated: