OpenFeign引起的HTTP Status 400與Tomcat吞沒資料

語言: CN / TW / HK

OpenFeign攔截器

在微服務中比較常見的場景:前端帶了JWT令牌請求服務A,在服務A中使用Feign遠端呼叫服務B、服務C等,A、B、C都接入了Spring Security;此時就會存在這樣的需求,如服務A呼叫服務B、C時不帶有JWT令牌就會出現服務呼叫失敗,無法通過服務B、C鑑權認證;

此時需要通過Feign提供的RequestInterceptor攔截器將A請求頭中所持有的Token在Feign發起遠端呼叫時繼續傳遞給服務B、服務C;

Demo示例程式碼:

public class DemoRequestInterceptor implements RequestInterceptor {

private final BearerTokenResolver tokenResolver;
@Override
public void apply(RequestTemplate template) {
    ServletRequestAttributes  attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request= attributes.getRequest();
    //獲取當前請求header
    Enumeration<String> headerNames = request.getHeaderNames();
    if (headerNames != null) {
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            String values = request.getHeader(name);
            //將header沿Feign呼叫鏈傳遞
            template.header(name, values);
        }
    }
 }
}

導致的問題

這種簡單粗暴全部將Header向下傳遞的方法將出現意想不到的副作用,導致程式請求呼叫失敗;

上面Demo程式碼,如發生如上圖所示的服務呼叫,將產生服務呼叫失敗,HTTP 400異常;

以下分析環境為Spring Boot2.7,使用內建tomcat 9.0.65;

  1. 客戶端Put請求,content-length等於74

  2. 全部拷貝UpdateA請求所攜帶的Header頭,服務A發起feign呼叫服務B,Get請求;

  3. 服務A發起feign呼叫服務C,Put請求;

服務A呼叫服務B成功完成請求,在服務A繼續發起的feign呼叫服務C出現如下異常,Tomcat無法解析該請求,丟擲異常,此時請求頭已經被破壞:

java.lang.IllegalArgumentException: Invalid character found in method name [late, ]. HTTP method names must be tokens
at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:421)

從異常中明顯可以看出Tomcat在解析請求行時出現異常,但具體什麼原因引起的還將要debug Tomcat原始碼才能發現問題所在;

問題跟蹤

在Tomcat中從丟擲異常的類處新增日誌輸出,跟蹤問題;

1、在Http11InputBuffer類的parseRequestLine方法中新增列印日誌:可看到兩個請求的請求頭資訊:

服務A對服務B發起的Get請求資訊,如上所述,Header頭全部帶上了:

2022-09-10 15:50:21.642 DEBUG 12028 --- [nio-8085-exec-5] 
o.a.coyote.http11.Http11InputBuffer 
:2:parsingRequestLinePhase--Received [GET /infos? 
pageSize=10&pageNo=1 HTTP/1.1
accept: */*
accept-encoding: gzip, deflate, br
authorization: Bearer admin11::ab5050d3-3542-4ada-80e9- 
6241132de5e5
cache-control: no-cache
connection: keep-alive
content-length: 74
content-type: application/json
cookie: JSESSIONID=3A1FD18830F43E295F07E8F77C0FA9FF
host: 127.0.0.1:7730
postman-token: fe8cfff4-e10b-4286-98da-d6d7bc05c006
user-agent: PostmanRuntime/7.29.2

服務A對服務C發起的Put請求資訊,但請求頭並不完整;請求行已不見,請求頭也只剩部分,請求體完整,這也是為什麼丟擲請求行解析失敗的原因;

2022-09-10 15:50:39.126 DEBUG 12028 --- [nio-8085-exec-5] o.a.coyote.http11.Http11InputBuffer      
:2:parsingRequestLinePhase--Received [late, br
authorization: Bearer admin11::ab5050d3-3542-4ada-80e9- 
6241132de5e5
cache-control: no-cache
connection: keep-alive
cookie: JSESSIONID=3A1FD18830F43E295F07E8F77C0FA9FF
host: 127.0.0.1:7730
postman-token: fe8cfff4-e10b-4286-98da-d6d7bc05c006
user-agent: PostmanRuntime/7.29.2
Content-Type: application/json
Content-Length: 34

{"aaabbbbId":123,"varray":["123"]}]

第一個Get請求,雖然附加了content-length: 74,但並不影響請求;第二個Put請求,請求頭直接被破壞;通過private boolean fill(boolean block)方法的日誌發現,Put請求接收時HTTP請求頭還是完整的;

在Parameters類的processParameters方法中並沒有對Get請求讀取Content-Length長度的資料,目前只能從Get請求完成之後、Put頭解析之前的程式碼進行跟蹤分析,還是在Http11InputBuffer類中,在每個請求結束之後都會執行如下方法:

/**
* 結束請求,消耗剩餘位元組
**/
void endRequest() throws IOException {
   //吞沒機制是否開啟
    if (swallowInput && (lastActiveFilter != -1)) {
        int extraBytes = (int) activeFilters[lastActiveFilter].end();
        byteBuffer.position(byteBuffer.position() - extraBytes);
    }
}
此方法將會根據吞沒機制配置與remaining位元組數對byteBuffer資料進行吞沒;

根據吞沒配置與上個請求完成後所剩餘的位元組數remaining,對資料byteBuffer資料進行吞沒;

在此可看到吞沒掉的位元組數為74,為第一個請求頭所攜帶的contentLength資料,第二個Put請求頭所丟失的資料;

如Feign呼叫為Get、Put、Put請求;Get、Get、Get請求將不會有異常情況出現,上述異常情況為:Put、Get、Put請求;之所以remaining會剩餘74是因為Tomcat並不會對Get請求從byteBuffer讀取Content-Length所傳輸的位元組資料;當請求沒有導致吞沒機制發生時就不會出現異常情況;