Websocket提示Expected ‘Connection‘ header value ‘Upgrade‘ but was ‘close‘

App使用Websocket和后台通信,我的项目使用的是Stomp for Android(stomp 为websoket添加心跳连接的逻辑)
stomp框架里面支持两种网络请求框架来进行webscoket通信,一个是java websocket框架,一个是okhttp。
在websocket的协议里面,在连接握手的时候,通过Draft的调用进行对response code和headers的检查;
在okhttp里面,这个逻辑是在okhttp3.internal.ws.RealWebSocket的checkUpgradeSuccess方法里面,

 @Throws(IOException::class)
  internal fun checkUpgradeSuccess(response: Response, exchange: Exchange?) {
    if (response.code != 101) {
      throw ProtocolException(
          "Expected HTTP 101 response but was '${response.code} ${response.message}'")
    }
 val headerConnection = response.header("Connection")
    if (!"Upgrade".equals(headerConnection, ignoreCase = true)) {
      throw ProtocolException(
          "Expected 'Connection' header value 'Upgrade' but was '$headerConnection'")
    }

    val headerUpgrade = response.header("Upgrade")
    if (!"websocket".equals(headerUpgrade, ignoreCase = true)) {
      throw ProtocolException(
          "Expected 'Upgrade' header value 'websocket' but was '$headerUpgrade'")
    }

    val headerAccept = response.header("Sec-WebSocket-Accept")
    val acceptExpected = (key + WebSocketProtocol.ACCEPT_MAGIC).encodeUtf8().sha1().base64()
    if (acceptExpected != headerAccept) {
      throw ProtocolException(
          "Expected 'Sec-WebSocket-Accept' header value '$acceptExpected' but was '$headerAccept'")
    }

    if (exchange == null) {
      throw ProtocolException("Web Socket exchange missing: bad interceptor?")
    }
  }

以上是okhttp 4.8.0的代码,别的版本的代码大同小异
大概握手的检查步骤如下
(1)检查response code是不是101
(2)检查header "Connection"的值是不是“Upgrade”
(3)检查header “Upgrade"的值是不是“websocket”
(4)检查"Sec-WebSocket-Accept”(这个步骤在很老的okhttp版本不存在)

不知道是什么原因,以前我们这边服务器返回的是正常的,webscoket可以正常连接和接收数据,而有一天安卓的app反馈数据不能正常显示,经过测试发现是websocket握手失败,而Ios端和web端却是正常的,经过抓包,发现服务器返回的报文中的报头发生了变化,其中“Connection”为key的header有两个,一个是“Upgrade”,一个是“Close”,经过以上okhttp的代码可以看出来,在进行websocket握手验证的时候,如果header"Connection"对应的值不是“Upgrade”,握手就会失败,然后连接会被关闭,websocket握手宣告失败。因为okhttp里面没有先获取去header"Connection"对应的所有值,然后遍历这些值,来找是否包含“Upgrade”,所以可以算是错误判断,但是问题是后台的某些问题导致的,而我这边后台一直没有办法定位到问题是怎么发生的。
我认为就是因为header的问题导致我这里websocket握手失败,但是经过尝试,我没有办法直接把okhttp源码从github下载下来直接作为module来依赖到项目离,然后对源码进行直接更改,因为里面缺少很多类导致build不成功,也就是说okhttp框架是不能轻易更改的,问题就卡在这里。
在无计可施之际,我想到ios的也是可以正常使用websocket的,这是为什么呢?我注意到stomp框架支持okhttp以及java websocket来实现websocket的通信功能,所以我在stomp里面,把网络框架切换到了java websocket,发现webscooket可以正常使用了!



我去研究了java websocket的代码,发现java websocket之所以没问题,是因为它的逻辑是获取headers “Connection”对应的所有value,然后使用"contains"方法来进行握手判断,代码如下:

	@Override
public HandshakeState acceptHandshakeAsClient( ClientHandshake request, ServerHandshake response ) throws InvalidHandshakeException {
	//从这个方法里面对headers里面的“Connection”和“Upgrade”进行检查
	if (! basicAccept( response )) {
		return HandshakeState.NOT_MATCHED;
		}
	if( !request.hasFieldValue( "Sec-WebSocket-Key" ) || !response.hasFieldValue( "Sec-WebSocket-Accept" ) 
	)
		return HandshakeState.NOT_MATCHED;

	String seckey_answere = response.getFieldValue( "Sec-WebSocket-Accept" );
	String seckey_challenge = request.getFieldValue( "Sec-WebSocket-Key" );
	seckey_challenge = generateFinalKey( seckey_challenge );

	if( !seckey_challenge.equals( seckey_answere ) )
		return HandshakeState.NOT_MATCHED;

	String requestedExtension = response.getFieldValue( "Sec-WebSocket-Extensions" );
	for( IExtension knownExtension : knownExtensions ) {
		if( knownExtension.acceptProvidedExtensionAsClient( requestedExtension ) ) {
			extension = knownExtension;
			return HandshakeState.MATCHED;
		}
	}
	return HandshakeState.NOT_MATCHED;
}
/*使用contais方法检查"Connection"里面有没有“Upgrade”,即使里面有包含“close”的值,也不会影响判断结果,从代码可以看出,这是有意而为之,因为在判断“Updrade”的key是否对应“websocket”的时候并没有用这个contains方法,而是用的equalsIgnoreCase
*/
protected boolean basicAccept( Handshakedata handshakedata ) {
	return handshakedata.getFieldValue( "Upgrade" ).equalsIgnoreCase( "websocket" ) && handshakedata.getFieldValue( "Connection" ).toLowerCase( Locale.ENGLISH ).contains( "upgrade" );
}
以上这段代码出自
org.java_websocket.drafts.Draft_6455,

org.java_websocket.client.WebSocketClient 是java websocket里面的作用就相当于okhttp中的OkHttpClient,其中虽然没有直接对握手进行判断的逻辑,但是通过构造函数中接收
一个Draft的参数,来把判断权交给了外部,通过调用Draft.acceptHandshakeAsClient的方法来过滤不正确的握手数据,以下是WebSocketClient的构造函数,在stomp框架里面,就是以Draft_6455作为参数传进来的,Draft_6455是Draft的子类
public WebSocketClient( URI serverUri , Draft protocolDraft , Map<String,String> httpHeaders , int connectTimeout ) {
		if( serverUri == null ) {
			throw new IllegalArgumentException();
		} else if( protocolDraft == null ) {
			throw new IllegalArgumentException( "null as draft is permitted for `WebSocketServer` only!" );
		}
		this.uri = serverUri;
		this.draft = protocolDraft;
		this.headers = httpHeaders;
		this.connectTimeout = connectTimeout;
		setTcpNoDelay( false );
		setReuseAddr( false );
		this.engine = new WebSocketImpl( this, protocolDraft );
	}

java websocket的github 地址
stomp github地址
虽然通过更改网络框架的方式把眼前的问题解决了,但是okhttp无法连接websocket的问题还是让我耿耿于怀,后台那边还是没有找到问题处在那里,我注意到java websocket里面使用draft来使握手判定的逻辑变得很灵活,而OkhttpClinet是使用了RealWebsocket,我想试图重写OkHttpClient来影响对RealWebsocket的调用,结果成功了,OkHttpClient在以下方法中调用了RealWebsocket:

 /** Uses [request] to connect a new web socket. */
  override fun newWebSocket(request: Request, listener: WebSocketListener): WebSocket {
    val webSocket = RealWebSocket(
        taskRunner = TaskRunner.INSTANCE,
        originalRequest = request,
        listener = listener,
        random = Random(),
        pingIntervalMillis = pingIntervalMillis.toLong(),
        extensions = null, // Always null for clients.
        minimumDeflateSize = minWebSocketMessageToCompress
    )
    webSocket.connect(this)
    return webSocket
  }

只要重写了这个方法,就可以修改对握手的判断,而RealWebsocket里面的逻辑不只包含了握手判断,还包含在连接成功之后如何发消息,如何接收消息,我想到要通过继承RealWebsocket来只对握手判断的逻辑做修改,然而,RealWebscket被okhttp设置为了不可继承的类,我注意到RealWebscooket其实是一个实现了okhttp3.WebSocket接口的抽象类,而只要我实现了okhttp3.WebSocket,也就可以取代RealWebscooket,可实际的逻辑,我可以复制RealWebscooket里面的方法,这样可行吗?
答案是可行,但是由于我创建的类并不处在okhttp的框架包名下,RealWebscooket内部使用的相关类很多是没有暴露在外部的,我根本无从获取,如果我不去直接修改okhttp的框架,把我的类加进去,就没办法真正取代RealWebscooket。
这时,我想到另一个方法,类似一个代理,我创建一个实现了okhttp3.WebSocke接口的类,然后在内部实例化RealWebscooket,让这个RealWebscooket来处理其它的逻辑,我只专注于对握手判定这里进行修改。我要修改的地方就是以下方法:

fun connect(client: OkHttpClient) {
    if (originalRequest.header("Sec-WebSocket-Extensions") != null) {
      failWebSocket(ProtocolException(
          "Request header not permitted: 'Sec-WebSocket-Extensions'"), null)
      return
    }

    val webSocketClient = client.newBuilder()
        .eventListener(EventListener.NONE)
        .protocols(ONLY_HTTP1)
        .build()
    val request = originalRequest.newBuilder()
        .header("Upgrade", "websocket")
        .header("Connection", "Upgrade")
        .header("Sec-WebSocket-Key", key)
        .header("Sec-WebSocket-Version", "13")
        .header("Sec-WebSocket-Extensions", "permessage-deflate")
        .build()
    call = RealCall(webSocketClient, request, forWebSocket = true)
    call!!.enqueue(object : Callback {
      override fun onResponse(call: Call, response: Response) {
        val exchange = response.exchange
        val streams: Streams
        try {
          checkUpgradeSuccess(response, exchange)
          streams = exchange!!.newWebSocketStreams()
        } catch (e: IOException) {
          exchange?.webSocketUpgradeFailed()
          failWebSocket(e, response)
          response.closeQuietly()
          return
        }

        // Apply the extensions. If they're unacceptable initiate a graceful shut down.
        // TODO(jwilson): Listeners should get onFailure() instead of onClosing() + onClosed(1010).
        val extensions = WebSocketExtensions.parse(response.headers)
        this@RealWebSocket.extensions = extensions
        if (!extensions.isValid()) {
          synchronized(this@RealWebSocket) {
            messageAndCloseQueue.clear() // Don't transmit any messages.
            close(1010, "unexpected Sec-WebSocket-Extensions in response header")
          }
        }

        // Process all web socket messages.
        try {
          val name = "$okHttpName WebSocket ${request.url.redact()}"
          initReaderAndWriter(name, streams)
          listener.onOpen(this@RealWebSocket, response)
          loopReader()
        } catch (e: Exception) {
          failWebSocket(e, null)
        }
      }

      override fun onFailure(call: Call, e: IOException) {
        failWebSocket(e, null)
      }
    })
  }

可以看到这个方法也不是可以随随便便可以重写的,因为里面涉及到很多RealWebscooket的私有字段和私有方法,如果我生生地重写了这个方法,存在两个问题(1)RealWebscooket里面的私有字段没有被赋值,那么我等我调用RealWebscooket地方法地时候,方法就会报错,经过测试,确实如此;
(2)RealWebsocket的私有方法恰恰是webscoket握手验证的核心,其中可以调用到okhttp隐藏的其它类相应的方法和参数,而这恰恰也是我依赖RealWebscoket的原因
我其实真正需要修改的只是这个方法里面涉及的checkUpgradeSuccess这个方法,其它的都不能动。为了不对RealWebsocket的逻辑进行干扰,我想到了反射,这样做的直接好处就是解决眼前遇到的问题,这样做的缺点也很严重,即使如果okhttp的版本就行了修改,这些反射可能回出现各种报错,导致握手失败。但是我顾及不了那么多了,我只是想解决眼前的问题,我通过反射做了以下几件事
(1)对以上方法中涉及到RealWebscooket私有字段赋值的操作进行反射
(2)对以上方法中需要RealWebscooket私有字段的部分通过反射获取私有字段
(3)通过反射调用以上方法中涉及的RealWebscooket私有方法
同时,还做了一些辅助的操作:
找到方法中用到的常量,这些常量属于okhttp的隐藏常量,通过查看源码,把这些常量赋值到我自己的类里面以备调用(这些常量或许回随着Okhttp版本变化而变化,这也存在了兼容性的风险)
在stomp框架中,我在OkhttpClient生成的地方,替换成了我自定义的OkhttpClient:

 /**
     * {@code webSocketClient} can accept the following type of clients:
     * <ul>
     * <li>{@code org.java_websocket.WebSocket}: cannot accept an existing client</li>
     * <li>{@code okhttp3.WebSocket}: can accept a non-null instance of {@code okhttp3.OkHttpClient}</li>
     * </ul>
     *
     * @param connectionProvider connectionProvider method
     * @param uri                URI to connect
     * @param connectHttpHeaders HTTP headers, will be passed with handshake query, may be null
     * @param okHttpClient       Existing client that will be used to open the WebSocket connection, may be null to use default client
     * @return StompClient for receiving and sending messages. Call #StompClient.connect
     */
    public static StompClient over(@NonNull ConnectionProvider connectionProvider, String uri, @Nullable Map<String, String> connectHttpHeaders, @Nullable OkHttpClient okHttpClient) {
        if (connectionProvider == ConnectionProvider.JWS) {
            if (okHttpClient != null) {
                throw new IllegalArgumentException("You cannot pass an OkHttpClient when using JWS. Use null instead.");
            }
            return createStompClient(new WebSocketsConnectionProvider(uri, connectHttpHeaders));
        }
	//使用继承自OkHttpClinet的MyOkhttpClient
        if (connectionProvider == ConnectionProvider.OKHTTP) {
            return createStompClient(new OkHttpConnectionProvider(uri, connectHttpHeaders, (okHttpClient == null) ? new MyOkhttpClient() : okHttpClient));
        }

        throw new IllegalArgumentException("ConnectionProvider type not supported: " + connectionProvider.toString());
    }

通过以上一系列的操作,经过测试,okhttp框架的websocket连接正常,接收数据正常

以下是在okhttp 4.8版本下我对OkHttpClinet的继承一起对RealWebscoket的调用的代码,如果遇到同样问题的朋友,可以参考一下

package ua.naiksoftware.stomp.provider

import android.annotation.SuppressLint
import android.nfc.Tag
import android.util.Log
import okhttp3.*
import okhttp3.EventListener
import okhttp3.internal.closeQuietly
import okhttp3.internal.concurrent.TaskRunner
import okhttp3.internal.connection.Exchange
import okhttp3.internal.connection.RealCall
import okhttp3.internal.ws.*
import okio.ByteString
import okio.ByteString.Companion.encodeUtf8
import okio.ByteString.Companion.toByteString
import okio.IOException
import java.net.ProtocolException
import java.util.*

class MyOkhttpClient : OkHttpClient() {
    var listener: WebSocketListener?=null;
    override fun newWebSocket(request: Request, listener: WebSocketListener): WebSocket {
        val webSocket = MyWebscoket(
                taskRunner = TaskRunner.INSTANCE,
                originalRequest = request,
                listener = listener,
                random = Random(),
                pingIntervalMillis = pingIntervalMillis.toLong(),
                extensions = null, // Always null for clients.
                minimumDeflateSize = minWebSocketMessageToCompress
        )
        webSocket.connect(this)
        return webSocket


    }

    class MyWebscoket(taskRunner: TaskRunner,
                      /** The application's original request unadulterated by web socket headers. */
                      private val originalRequest: Request,
                      internal val listener: WebSocketListener,
                      private val random: Random,
                      private val pingIntervalMillis: Long,
                      /**
                       * For clients this is initially null, and will be assigned to the agreed-upon extensions. For
                       * servers it should be the agreed-upon extensions immediately.
                       */
                      private var extensions: WebSocketExtensions?,
                      /** If compression is negotiated, outbound messages of this size and larger will be compressed. */
                      private var minimumDeflateSize: Long) :WebSocket{

        private val key: String
        private val realWebsocket:RealWebSocket
        init {
            require("GET" == originalRequest.method) {
                "Request must be GET: ${originalRequest.method}"
            }

            this.key = ByteArray(16).apply { random.nextBytes(this) }.toByteString().base64()
            realWebsocket = RealWebSocket(taskRunner, originalRequest, listener, random, pingIntervalMillis, extensions, minimumDeflateSize)
        }
        internal val okHttpName =
                OkHttpClient::class.java.name.removePrefix("okhttp3.").removeSuffix("Client")
        companion object {
            internal const val ACCEPT_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
            private val ONLY_HTTP1 = listOf(Protocol.HTTP_1_1)

            /**
             * The maximum number of bytes to enqueue. Rather than enqueueing beyond this limit we tear down
             * the web socket! It's possible that we're writing faster than the peer can read.
             */
            private const val MAX_QUEUE_SIZE = 16L * 1024 * 1024 // 16 MiB.

            /**
             * The maximum amount of time after the client calls [close] to wait for a graceful shutdown. If
             * the server doesn't respond the web socket will be canceled.
             */
            private const val CANCEL_AFTER_CLOSE_MILLIS = 60L * 1000

            /**
             * The smallest message that will be compressed. We use 1024 because smaller messages already
             * fit comfortably within a single ethernet packet (1500 bytes) even with framing overhead.
             *
             * For tests this must be big enough to realize real compression on test messages like
             * 'aaaaaaaaaa...'. Our tests check if compression was applied just by looking at the size if
             * the inbound buffer.
             */
            const val DEFAULT_MINIMUM_DEFLATE_SIZE = 1024L
        }
        var call:RealCall?=null;
        @SuppressLint("NewApi")
        fun  connect(client:OkHttpClient){

            if (originalRequest.header("Sec-WebSocket-Extensions") != null) {
                failWebSocket(ProtocolException(
                        "Request header not permitted: 'Sec-WebSocket-Extensions'"), null)
                return
            }

            val webSocketClient = client.newBuilder()
                    .eventListener(EventListener.NONE)
                    .protocols(ONLY_HTTP1)
                    .build()
            val request = originalRequest.newBuilder()
                    .header("Upgrade", "websocket")
                    .header("Connection", "Upgrade")
                    .header("Sec-WebSocket-Key", key)
                    .header("Sec-WebSocket-Version", "13")
                    .header("Sec-WebSocket-Extensions", "permessage-deflate")
                    .build()
           val  call = RealCall(webSocketClient, request, forWebSocket = true)
            MyWebscokoetHelper.setFailed(call,"call",realWebsocket)//把值设置到realWebsocket里面
            call!!.enqueue(object : Callback {
                override fun onResponse(call: Call, response: Response) {
                    val exchange:Exchange = MyWebscokoetHelper.getFailed("exchange",response) as Exchange
                    val streams: RealWebSocket.Streams
                    try {
                        checkUpgradeSuccess(response, exchange)
                        streams = exchange!!.newWebSocketStreams()
                    } catch (e: IOException) {
                        exchange?.webSocketUpgradeFailed()
                        failWebSocket(e, response)
                        response.closeQuietly()
                        return
                    }

                    // Apply the extensions. If they're unacceptable initiate a graceful shut down.
                    // TODO(jwilson): Listeners should get onFailure() instead of onClosing() + onClosed(1010).
                    val extensions = WebSocketExtensions.parse(response.headers)
                    MyWebscokoetHelper.setFailed(extensions,"extensions",realWebsocket)
                    if (!MyWebscokoetHelper.isValid(realWebsocket,extensions)) {
                        synchronized(realWebsocket) {
                             val messageAndCloseQueue :ArrayDeque<Any> = MyWebscokoetHelper.getFeild("messageAndCloseQueue",realWebsocket) as ArrayDeque<Any>
                            messageAndCloseQueue.clear() // Don't transmit any messages.
                            close(1010, "unexpected Sec-WebSocket-Extensions in response header")
                        }
                    }

                    // Process all web socket messages.
                    try {
                        val name = "$okHttpName WebSocket ${request.url.redact()}"
                       realWebsocket.initReaderAndWriter(name, streams)
                        listener.onOpen(realWebsocket, response)
                        realWebsocket.loopReader()
                    } catch (e: Exception) {
                        Log.d("MyWebscoket","146 "+e.localizedMessage)
                        failWebSocket(e, null)
                    }
                }

                override fun onFailure(call: Call, e: IOException) {
                    failWebSocket(e, null)
                }
            })
        }

        private fun checkUpgradeSuccess(response: Response, exchange: Exchange?) {
            if (response.code != 101) {
                throw ProtocolException(
                        "Expected HTTP 101 response but was '${response.code} ${response.message}'")
            }
            val list:List<String> =  response.headers("Connection")
            var haveUpgrade = false;
            for (h in list){
                if ("Upgrade".equals(h, ignoreCase = true)) {
                    haveUpgrade = true;
                }
            }
            if(!haveUpgrade)
                throw ProtocolException(
                        "Expected 'Connection' header value 'Upgrade' but was ${response.header("Connection")}")

            val headerUpgrade = response.header("Upgrade")
            if (!"websocket".equals(headerUpgrade, ignoreCase = true)) {
                throw ProtocolException(
                        "Expected 'Upgrade' header value 'websocket' but was '$headerUpgrade'")
            }

            val headerAccept = response.header("Sec-WebSocket-Accept")
            val acceptExpected = (key + ACCEPT_MAGIC).encodeUtf8().sha1().base64()
            if (acceptExpected != headerAccept) {
                throw ProtocolException(
                        "Expected 'Sec-WebSocket-Accept' header value '$acceptExpected' but was '$headerAccept'")
            }

            if (exchange == null) {
                throw ProtocolException("Web Socket exchange missing: bad interceptor?")
            }

        }

        fun failWebSocket(e: Exception, response: Response?) {
            realWebsocket.failWebSocket(e,response)
        }
        override fun cancel() {
            realWebsocket.cancel()
        }

        override fun close(code: Int, reason: String?): Boolean {
            return realWebsocket.close(code, reason)
        }

        override fun queueSize(): Long {
          return realWebsocket.queueSize()
        }

        override fun request(): Request {
          return realWebsocket.request()
        }

        override fun send(text: String): Boolean {
            return realWebsocket.send(text)
        }

        override fun send(bytes: ByteString): Boolean {
            return realWebsocket.send(bytes)
        }
    }
}

以下是涉及到RealWebscoket的反射处理的工具类代码

package ua.naiksoftware.stomp.provider;

import android.annotation.SuppressLint;
import android.os.Build;

import androidx.annotation.RequiresApi;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import okhttp3.Response;
import okhttp3.internal.ws.RealWebSocket;
import okhttp3.internal.ws.WebSocketExtensions;

public class MyWebscokoetHelper {
    /**
     * 给RealWebSocket的内部字段赋值
     * @param filedValue
     * @param filedName
     * @param realWebSocket
     */
    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    public static void setFailed(Object filedValue, String filedName, RealWebSocket realWebSocket){
        Class clazz = RealWebSocket.class;
        try {
            Field field = clazz.getDeclaredField(filedName);
            field.setAccessible(true);
            field.set(realWebSocket,filedValue);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    public static Object getFailed(String filedName, Response realWebSocket){
        Class clazz = Response.class;

        Field field = null;
        try {
            field = clazz.getDeclaredField(filedName);
            field.setAccessible(true);
            return field.get(realWebSocket);
        } catch (@SuppressLint("NewApi") NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;


    }
    public static boolean isValid(RealWebSocket realWebSocket,WebSocketExtensions extensions){
        Class clazz = RealWebSocket.class;
        try {
            Method[] methods = clazz.getDeclaredMethods();
            for (Method m:methods) {
                String name = m.getName();
                if(name.equals("isValid")){
                    m.setAccessible(true);
                    Class[] types = m.getParameterTypes();
                    return (boolean) m.invoke(realWebSocket,extensions);
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return false;
    }
    public static Object getFeild(String failedName,RealWebSocket realWebSocket){
        Class clazz = RealWebSocket.class;
        Field field = null;
        try {
            field = clazz.getDeclaredField(failedName);
            field.setAccessible(true);
            return field.get(realWebSocket);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
}

直到目前,我还是不清楚后台返回header问题的原因是什么,但是app这边已经尽力了,如果有大神清楚其中的原因,欢迎提点

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值