Análisis y solución de la respuesta 400 de nginx provocada por una solicitud HTTP irregular

fondo

Recientemente, al analizar los datos, descubrí accidentalmente que hay un grupo de usuarios en el registro de nginx. Todas las solicitudes de informes del registro HTTP POST devuelven 400 y no hay un registro de éxito de 200. Dado que solo representa menos del 0,5% de las solicitudes totales. , La alarma de monitoreo no se ha activado antes, y es muy común. Lo extraño es que solo para la interfaz POST informada por el registro, habrá esta situación de los 400 para un usuario específico, pero no existe tal problema. para otras interfaces, ya sea POST o GET.

Un análisis más detallado del registro reveló que, de hecho, para las solicitudes de los usuarios en algunas áreas, esta proporción incluso excedía el 10%, por lo que me tomé el tiempo para hacer un seguimiento y finalmente descubrí que se debía al formato no estándar de las solicitudes HTTP emitidas por algunos modelos de clientes, lo dejaré aquí, analizar el proceso, causas y soluciones finales.

análisis del problema

Razones comunes de nginx 400

Después de buscar información en línea, descubrí que puede haber varias razones por las que nginx responde con 400:

  1. request_uri es demasiado largo y excede el tamaño de configuración de nginx
  2. La cookie o el encabezado es demasiado grande y excede el tamaño de configuración de nginx.
  3. Encabezado HOST vacío
  4. content_length y body length son inconsistentes

Estos errores en realidad ocurren en la capa nginx, es decir, cuando nginx procesa, piensa que el formato de solicitud del cliente es incorrecto, por lo que devuelve directamente 400 y no reenvía la solicitud al servidor ascendente, por lo que el servidor ascendente no lo sabe por completo. de estas solicitudes erróneas.

Esta vez, de acuerdo con el análisis del registro de nginx, podemos ver que nginx realmente reenvía la solicitud al servidor ascendente: upstream_addr ya es la dirección efectiva del servidor ascendente, por lo que el servidor ascendente debería devolver el 400 en lugar de nginx directamente. Esto muestra que al menos una capa de nginx cree que el formato de la solicitud está bien.

Análisis de registro real de nginx 400

Intercepte los registros de errores de algunos usuarios en línea. El formato general es el siguiente:

127.0.0.1:63646	-	24/Apr/2022:00:50:07 +0900	127.0.0.1:1080	0.000	0.000	POST /log/report?appd=abc.demo.android&appname=abcdemo&v=1.0&langes=zh-CN&phonetype=android&device_type=android&osn=Android OS 10 / API-29 (QKQ1.190825.002/V12.0.6.0.QFKCNXM)&channel=Google Play&build=Android OS 10 / API-29 (QKQ1.190825.002/V12.0.6.0.QFKCNXM)&resolution=1080x2340&ts=1650636192534 HTTP/1.1	400	50	-	curl/7.52.1	-	0.000	0.000	127.0.0.1	1563	2021

El análisis de registros puede revelar que la mayoría de las solicitudes 400 tienen un problema: sus parámetros de consulta no se han codificado en URL. Por ejemplo, puede ver claramente que los espacios en el parámetro canal=Google Play no se han transcodificado a %20. Intuitivamente, esto debería ser lo mismo que La razón de 400 está directamente relacionada.

ensayo

Para verificar si el parámetro de consulta no transcodificado es la causa directa de 400, simplemente cree varias solicitudes http de prueba a través de curl:

# 无空格
curl -v 'http://127.0.0.1/log/report?appd=abc.demo.android&appname=abcdemo&v=1.0&langes=zh-CN&phonetype=android&channel=Google%20Play' -d @test.json
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> POST /log/report?appd=abc.demo.android&appname=abcdemo&v=1.0&langes=zh-CN&phonetype=android&channel=Google%20Play HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.52.1
> Accept: */*
> Content-Length: 1563
> Content-Type: application/x-www-form-urlencoded
> Expect: 100-continue
>
< HTTP/1.1 100 Continue
* We are completely uploaded and fine
< HTTP/1.1 200 OK
< Server: nginx/1.16.1
< Date: Sat, 23 Apr 2022 15:54:53 GMT
< Content-Type: application/json
< Content-Length: 22
< Connection: keep-alive
<
* Curl_http_done: called premature == 0
* Connection #0 to host 127.0.0.1 left intact
# 有空格
curl -v 'http://127.0.0.1/log/report?appd=abc.demo.android&appname=abcdemo&v=1.0&langes=zh-CN&phonetype=android&channel=Google Play' -d @test.json
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> POST /log/report?appd=abc.demo.android&appname=abcdemo&v=1.0&langes=zh-CN&phonetype=android&channel=Google Play HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.52.1
> Accept: */*
> Content-Length: 1563
> Content-Type: application/x-www-form-urlencoded
> Expect: 100-continue
>
< HTTP/1.1 100 Continue
* We are completely uploaded and fine
< HTTP/1.1 400 Bad Request
< Server: nginx/1.16.1
< Date: Sat, 23 Apr 2022 15:55:14 GMT
< Content-Type: text/plain; charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
<
* Curl_http_done: called premature == 0
* Connection #0 to host 127.0.0.1 left intact

Se descubre que todas las solicitudes con espacios en el servidor ascendente devolverán directamente 400. Se puede inferir que el parámetro de consulta no codificado en URL es la causa directa del problema 400, pero ¿por qué la transcodificación no causa 400? ¿Cómo explicar este fenómeno desde la perspectiva de los principios HTTP? Para encontrar la respuesta, debemos revisar el estándar del protocolo HTTP.

Formato de especificación de solicitud HTTP

El formato del mensaje de solicitud HTTP es el siguiente:


Como se muestra en la figura anterior, como protocolo de texto, la distinción y división de diferentes partes del mensaje de solicitud HTTP se basa completamente en las marcas de caracteres de espacios, retornos de carro\r y avances de línea\n. Para los tres caracteres en la primera línea La división de cada método de solicitud parcial, URL y versión del protocolo se divide en espacios.

Al analizar las 400 solicitudes HTTP encontradas, podemos encontrar que debido a que los parámetros de consulta no están codificados en URL, aparecerán espacios en ellos. Estrictamente hablando, esta solicitud ya no cumple con la especificación HTTP, porque en este momento, la primera línea se puede dividir según en los espacios para producir más de 3 La parte no puede corresponder uno a uno con el método, la URL y la versión. Desde un punto de vista semántico, es una lógica de procesamiento razonable devolver directamente 400 en este momento.

En el procesamiento real, cuando nos enfrentamos a esta situación, algunos componentes pueden ser compatibles: la primera y la última parte de la división se utilizan como método y versión respectivamente, y las partes restantes en el medio se unifican como URL. Por ejemplo, nginx es compatible con este formato no estándar, pero muchos componentes no son compatibles con esta situación; después de todo, esto no cumple con la especificación HTTP. Por ejemplo, la captura de paquetes de Charles provocará un error al realizar dicha solicitud. Golang's net/ La biblioteca http y el módulo http de Django informarán 400 cuando reciban dichas solicitudes.

golang net/http análisis de código HTTP

El servidor ascendente responsable de los informes de registros es logsvc implementado en golang. Utiliza la biblioteca de tarjetas estándar net/http para procesar las solicitudes HTTP. Exploremos más a fondo cómo la biblioteca estándar analiza las solicitudes HTTP para confirmar la causa del error.

Según el código fuente de golang, se puede encontrar que la ruta de análisis de la solicitud HTTP es http.ListenAndServe => http.Serve => server => readRequest.... La lógica de análisis del encabezado de la solicitud HTTP se encuentra en readRequest función.
La parte readRequest del código es la siguiente:

// file: net/http/request.go
...
1009 func readRequest(b *bufio.Reader, deleteHostHeader bool) (req *Request, err error) {
1010     tp := newTextprotoReader(b)
1011     req = new(Request)
1012
1013     // First line: GET /index.html HTTP/1.0
1014     var s string
1015     if s, err = tp.ReadLine(); err != nil {
1016         return nil, err
1017     }
1018     defer func() {
1019         putTextprotoReader(tp)
1020         if err == io.EOF {
1021             err = io.ErrUnexpectedEOF
1022         }
1023     }()
1024
1025     var ok bool
1026     req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(s)
1027     if !ok {
1028         return nil, &badStringError{"malformed HTTP request", s}
1029     }
1030     if !validMethod(req.Method) {
1031         return nil, &badStringError{"invalid method", req.Method}
1032     }
1033     rawurl := req.RequestURI
1034     if req.ProtoMajor, req.ProtoMinor, ok = ParseHTTPVersion(req.Proto); !ok {
1035         return nil, &badStringError{"malformed HTTP version", req.Proto}
1036     }
...

Puede ver que en readRequest, primero analice los tres campos de método, URL y Proto en la primera línea a través de parseRequestLine, y luego analice si la versión es correcta a través de ParseHTTPVersion.

El código parseRequestLine es el siguiente:

...
 966 // parseRequestLine parses "GET /foo HTTP/1.1" into its three parts.
 967 func parseRequestLine(line string) (method, requestURI, proto string, ok bool) {
 968     s1 := strings.Index(line, " ")
 969     s2 := strings.Index(line[s1+1:], " ")
 970     if s1 < 0 || s2 < 0 {
 971         return
 972     }
 973     s2 += s1 + 1
 974     return line[:s1], line[s1+1 : s2], line[s2+1:], true
 975 }

Se puede ver que el código de análisis de parseRequestLine es encontrar el índice de espacio 0 y 1 y luego cortarlo directamente en tres partes: método, requestURI y proto según la sintaxis del segmento. Si el requestURI contiene espacios adicionales, causará el valor proto En realidad se convierte en todos los caracteres después del primer espacio. Por ejemplo, "POST abc/?x=o space d HTTP/1.1" se analizará como: método=POST, requestURI=abc/?x=0, proto= "espacio d HTTP/1.1", lo que provocará un error en el análisis de ParseHTTPVersion en el siguiente paso.

El código de ParseHTTPVersion es el siguiente: se puede encontrar que si el campo de versión analizado por parseRequestLine no es legal, se devolverá un error:

...
 769 // ParseHTTPVersion parses an HTTP version string.
 770 // "HTTP/1.0" returns (1, 0, true).
 771 func ParseHTTPVersion(vers string) (major, minor int, ok bool) {
 772     const Big = 1000000 // arbitrary upper bound
 773     switch vers {
 774     case "HTTP/1.1":
 775         return 1, 1, true
 776     case "HTTP/1.0":
 777         return 1, 0, true
 778     }
 779     if !strings.HasPrefix(vers, "HTTP/") {
 780         return 0, 0, false
 781     }
 782     dot := strings.Index(vers, ".")
 783     if dot < 0 {
 784         return 0, 0, false
 785     }
 786     major, err := strconv.Atoi(vers[5:dot])
 787     if err != nil || major < 0 || major > Big {
 788         return 0, 0, false
 789     }
 790     minor, err = strconv.Atoi(vers[dot+1:])
 791     if err != nil || minor < 0 || minor > Big {
 792         return 0, 0, false
 793     }
 794     return major, minor, true
 795 }

solución

Lo primero que debe hacer es alinear el problema con el cliente. El cliente confirma que el método de la biblioteca de red que llama a Unity en algunos modelos no logra codificar correctamente sus parámetros de consulta. La nueva versión agregará código adicional encima de la biblioteca de red de Unity para garantizar todos los parámetros. Debe estar codificado en URL para que se ajuste a la especificación HTTP.

Luego considere más a fondo si es posible manejar temporalmente las solicitudes anormales existentes en línea para evitar la pérdida continua de datos reportada por esta parte del registro de usuario anormal antes de que se sobrescriba y repare la nueva versión. Se consideran las siguientes soluciones para compatibilidad

Pruebe la biblioteca golang HTTP de terceros gin && echo

Dado que el servicio de registro está a cargo de un servidor golang independiente, su lógica de código es muy simple: solo descomprime, analiza y escribe el cuerpo de la solicitud POST de registro en Kafka, sin ninguna otra lógica adicional, y el costo de modificación es bajo, por lo que se considera primero el reemplazo de net/http por otras bibliotecas de terceros para ver si pueden resolver el problema.

Probé las populares bibliotecas gin y echo y descubrí que ambas reportaban 400. No pude evitar explorar sus códigos fuente. Resultó que estas dos bibliotecas en realidad llamaban a los métodos ListenAndServer y Serve de net/http. La lógica de análisis anterior net /http corresponde al código responsable, por lo que naturalmente informará 400.

El script nginx lua/perl cambia los parámetros de consulta

Otro método posible que me viene a la mente es usar un script lua/perl en la capa nginx para codificar en urlen el parámetro request_uri entrante que no está codificado en urlen y luego enviarlo al servidor ascendente. Sin embargo, se descubrió que los módulos lua y perl eran no integrado durante la compilación de nginx en línea. Para utilizar este método, sólo puedes:

  1. Vuelva a compilar todo el nginx y reemplace el nginx original
  2. O use la carga dinámica para compilar los módulos perl y lua por separado y luego use nginx para cargarlos dinámicamente.

Teniendo en cuenta que soy un RD en lugar de un OP profesional de nginx y el riesgo de impacto en línea, no lo intentaré fácilmente.

nginx enruta el registro/informe a un servidor que sea compatible con solicitudes HTTP sin censura de espacios en blanco

Como se mencionó al principio, para solicitudes anormales que esperan espacios, solo la interfaz POST de informes de registro devolverá 400, y otras interfaces volverán a la normalidad. Esto en realidad se debe a que la interfaz comercial normal y la interfaz de registro se dividen durante el reenvío de nginx, registro/informe La interfaz se reenviará al servicio independiente golang logsvc por separado, y las solicitudes comerciales normales se reenviarán al servicio API principal de Python.
Mirando hacia atrás, la razón por la que dividimos un servidor golang separado para que sea responsable de analizar los informes de registro de aplicaciones y escribir Kafka, en lugar de que el servicio API principal sea responsable de otra lógica de interfaz, es principalmente por dos razones:

  1. El servicio principal API escrito en Pythono es relativamente ineficiente. Los informes de registros frecuentes y a gran escala pueden consumir demasiados recursos y ser lentos.
  2. Evite que las solicitudes de informes de registros afecten la velocidad de respuesta de otras solicitudes comerciales normales y desacople la lógica empresarial de los informes de registros.

El logsvc actual no puede manejar esta situación, pero el servicio principal de API que utiliza el protocolo uwsgi para interactuar con nginx se puede analizar normalmente, así que agregue la siguiente configuración temporal a nginx:

    location /log/report {
        include proxy_params;
        if ( $args !~ "^(.*) (.*)$" ) {
	    proxy_pass http://test_log_stream;
            break;
        }
        include uwsgi_params;
        uwsgi_pass test_api_stream;
    }

Es decir, si no hay espacios en los parámetros de consulta (args) a través de la coincidencia regular, será procesado directamente por logsvc. Si hay espacios, será procesado por el servicio principal de API utilizando el protocolo uwsgi. Dado que tales solicitudes anormales Solo representa menos del 0,5% del total de solicitudes. La arquitectura dividida considerada anteriormente todavía está funcionando, pero para una pequeña cantidad de solicitudes anormales, primero se procesa a través del servicio principal API para garantizar la compatibilidad.

Indique la fuente al reimprimir, dirección original:  https://www.cnblogs.com/AcAc-t/p/nginx_400_problem_for_not_encode_http_request.html

 

Supongo que te gusta

Origin blog.csdn.net/pantouyuchiyu/article/details/131440322
Recomendado
Clasificación