Reputation: 1483
I need to add custom headers to EVERY request coming from the WebView. I know loadURL
has the parameter for extraHeaders
, but those are only applied to the initial request. All subsequent requests do not contain the headers. I have looked at all overrides in WebViewClient
, but nothing allows for adding headers to resource requests - onLoadResource(WebView view, String url)
. Any help would be wonderful.
Thanks, Ray
Upvotes: 134
Views: 189082
Reputation: 133
You can try my solution for the issue, and while I acknowledge that it may not be universally applicable, it has proven effective in meeting my specific requirements, particularly when dealing with a significant number of JavaScript fetch calls that do not trigger WebViewClient callbacks.
To add custom headers into every WebView HTTP request, you can inject java script code that overrides window.fetch behavior in your own WebViewClient.
class CustomHeaderWebViewClient(headerValue: String) : WebViewClient() { }
In this custom WebViewClient, you can pass the headerValue that you want to inject into HTTP headers.
Create a property that stores your JavaScript injection code. This code will modify the window.fetch method to include the custom header:
private val authHeaderInjection = """
(function() {
const originalFetch = window.fetch;
window.fetch = function(input, init) {
init = init || {};
init.headers = init.headers || {};
init.headers['My-Header'] = '$headerValue';
return originalFetch.apply(this, arguments);
};
})();
""".trimIndent()
You need to override the onPageStarted method to execute the JavaScript injection on each navigation:
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
view?.evaluateJavascript(authHeaderInjection) { Timber.d(it) }
super.onPageStarted(view, url, favicon)
}
Note: Depending on the implementation of the WebView's content, you can choose different callbacks.
Note: Please note that you may still need to manually add other custom headers when using webview.loadUrl(url, yourHeaders)
as per your requirements.
import android.graphics.Bitmap
import android.webkit.WebView
import android.webkit.WebViewClient
import timber.log.Timber
class CustomHeaderWebViewClient(headerValue: String) : WebViewClient() {
private val authHeaderInjection = """
(function() {
const originalFetch = window.fetch;
window.fetch = function(input, init) {
init = init || {};
init.headers = init.headers || {};
init.headers['My-Header'] = '${headerValue}';
return originalFetch.apply(this, arguments);
};
})();
""".trimIndent()
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
view?.evaluateJavascript(authHeaderInjection) { Timber.d(it) }
super.onPageStarted(view, url, favicon)
}
}
Upvotes: 0
Reputation: 206
shouldInterceptRequest
could not intercept request.body
.
I recommend you reset userAgent before werview.loadUrl
.
like:
webview.settings.userAgentString += " key1/value1 key2/value2"
Upvotes: 0
Reputation: 4112
Here is an implementation using HttpUrlConnection:
class CustomWebviewClient : WebViewClient() {
private val charsetPattern = Pattern.compile(".*?charset=(.*?)(;.*)?$")
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
try {
val connection: HttpURLConnection = URL(request.url.toString()).openConnection() as HttpURLConnection
connection.requestMethod = request.method
for ((key, value) in request.requestHeaders) {
connection.addRequestProperty(key, value)
}
connection.addRequestProperty("custom header key", "custom header value")
var contentType: String? = connection.contentType
var charset: String? = null
if (contentType != null) {
// some content types may include charset => strip; e. g. "application/json; charset=utf-8"
val contentTypeTokenizer = StringTokenizer(contentType, ";")
val tokenizedContentType = contentTypeTokenizer.nextToken()
var capturedCharset: String? = connection.contentEncoding
if (capturedCharset == null) {
val charsetMatcher = charsetPattern.matcher(contentType)
if (charsetMatcher.find() && charsetMatcher.groupCount() > 0) {
capturedCharset = charsetMatcher.group(1)
}
}
if (capturedCharset != null && !capturedCharset.isEmpty()) {
charset = capturedCharset
}
contentType = tokenizedContentType
}
val status = connection.responseCode
var inputStream = if (status == HttpURLConnection.HTTP_OK) {
connection.inputStream
} else {
// error stream can sometimes be null even if status is different from HTTP_OK
// (e. g. in case of 404)
connection.errorStream ?: connection.inputStream
}
val headers = connection.headerFields
val contentEncodings = headers.get("Content-Encoding")
if (contentEncodings != null) {
for (header in contentEncodings) {
if (header.equals("gzip", true)) {
inputStream = GZIPInputStream(inputStream)
break
}
}
}
return WebResourceResponse(contentType, charset, status, connection.responseMessage, convertConnectionResponseToSingleValueMap(connection.headerFields), inputStream)
} catch (e: Exception) {
e.printStackTrace()
}
return super.shouldInterceptRequest(view, request)
}
private fun convertConnectionResponseToSingleValueMap(headerFields: Map<String, List<String>>): Map<String, String> {
val headers = HashMap<String, String>()
for ((key, value) in headerFields) {
when {
value.size == 1 -> headers[key] = value[0]
value.isEmpty() -> headers[key] = ""
else -> {
val builder = StringBuilder(value[0])
val separator = "; "
for (i in 1 until value.size) {
builder.append(separator)
builder.append(value[i])
}
headers[key] = builder.toString()
}
}
}
return headers
}
}
Note that this does not work for POST requests because WebResourceRequest doesn't provide POST data. There is a Request Data - WebViewClient library which uses a JavaScript injection workaround for intercepting POST data.
Upvotes: 5
Reputation: 5183
I came accross the same problem and solved.
As said before you need to create your custom WebViewClient and override the shouldInterceptRequest method.
WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)
That method should issue a webView.loadUrl while returning an "empty" WebResourceResponse.
Something like this:
@Override
public boolean shouldInterceptRequest(WebView view, WebResourceRequest request) {
// Check for "recursive request" (are yor header set?)
if (request.getRequestHeaders().containsKey("Your Header"))
return null;
// Add here your headers (could be good to import original request header here!!!)
Map<String, String> customHeaders = new HashMap<String, String>();
customHeaders.put("Your Header","Your Header Value");
view.loadUrl(url, customHeaders);
return new WebResourceResponse("", "", null);
}
Upvotes: -3
Reputation: 17557
Try
loadUrl(String url, Map<String, String> extraHeaders)
For adding headers to resources loading requests, make custom WebViewClient and override:
API 24+:
WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)
or
WebResourceResponse shouldInterceptRequest(WebView view, String url)
Upvotes: 97
Reputation: 661
Maybe my response quite late, but it covers API below and above 21 level.
To add headers we should intercept every request and create new one with required headers.
So we need to override shouldInterceptRequest method called in both cases: 1. for API until level 21; 2. for API level 21+
webView.setWebViewClient(new WebViewClient() {
// Handle API until level 21
@SuppressWarnings("deprecation")
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
return getNewResponse(url);
}
// Handle API 21+
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();
return getNewResponse(url);
}
private WebResourceResponse getNewResponse(String url) {
try {
OkHttpClient httpClient = new OkHttpClient();
Request request = new Request.Builder()
.url(url.trim())
.addHeader("Authorization", "YOU_AUTH_KEY") // Example header
.addHeader("api-key", "YOUR_API_KEY") // Example header
.build();
Response response = httpClient.newCall(request).execute();
return new WebResourceResponse(
null,
response.header("content-encoding", "utf-8"),
response.body().byteStream()
);
} catch (Exception e) {
return null;
}
}
});
If response type should be processed you could change
return new WebResourceResponse(
null, // <- Change here
response.header("content-encoding", "utf-8"),
response.body().byteStream()
);
to
return new WebResourceResponse(
getMimeType(url), // <- Change here
response.header("content-encoding", "utf-8"),
response.body().byteStream()
);
and add method
private String getMimeType(String url) {
String type = null;
String extension = MimeTypeMap.getFileExtensionFromUrl(url);
if (extension != null) {
switch (extension) {
case "js":
return "text/javascript";
case "woff":
return "application/font-woff";
case "woff2":
return "application/font-woff2";
case "ttf":
return "application/x-font-ttf";
case "eot":
return "application/vnd.ms-fontobject";
case "svg":
return "image/svg+xml";
}
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
return type;
}
Upvotes: 39
Reputation: 383
This works for me:
First you need to create method, which will be returns your headers you want to add to request:
private Map<String, String> getCustomHeaders()
{
Map<String, String> headers = new HashMap<>();
headers.put("YOURHEADER", "VALUE");
return headers;
}
Second you need to create WebViewClient:
private WebViewClient getWebViewClient()
{
return new WebViewClient()
{
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
{
view.loadUrl(request.getUrl().toString(), getCustomHeaders());
return true;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url)
{
view.loadUrl(url, getCustomHeaders());
return true;
}
};
}
Add WebViewClient to your WebView:
webView.setWebViewClient(getWebViewClient());
Hope this helps.
Upvotes: 18
Reputation: 29
This worked for me. Create WebViewClient like this below and set the webclient to your webview. I had to use webview.loadDataWithBaseURL as my urls (in my content) did not have the baseurl but only relative urls. You will get the url correctly only when there is a baseurl set using loadDataWithBaseURL.
public WebViewClient getWebViewClientWithCustomHeader(){
return new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
try {
OkHttpClient httpClient = new OkHttpClient();
com.squareup.okhttp.Request request = new com.squareup.okhttp.Request.Builder()
.url(url.trim())
.addHeader("<your-custom-header-name>", "<your-custom-header-value>")
.build();
com.squareup.okhttp.Response response = httpClient.newCall(request).execute();
return new WebResourceResponse(
response.header("content-type", response.body().contentType().type()), // You can set something other as default content-type
response.header("content-encoding", "utf-8"), // Again, you can set another encoding as default
response.body().byteStream()
);
} catch (ClientProtocolException e) {
//return null to tell WebView we failed to fetch it WebView should try again.
return null;
} catch (IOException e) {
//return null to tell WebView we failed to fetch it WebView should try again.
return null;
}
}
};
}
Upvotes: 2
Reputation: 701
You can use this:
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// Here put your code
Map<String, String> map = new HashMap<String, String>();
map.put("Content-Type","application/json");
view.loadUrl(url, map);
return false;
}
Upvotes: -2
Reputation: 59671
You will need to intercept each request using WebViewClient.shouldInterceptRequest
With each interception, you will need to take the url, make this request yourself, and return the content stream:
WebViewClient wvc = new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
try {
DefaultHttpClient client = new DefaultHttpClient();
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader("MY-CUSTOM-HEADER", "header value");
httpGet.setHeader(HttpHeaders.USER_AGENT, "custom user-agent");
HttpResponse httpReponse = client.execute(httpGet);
Header contentType = httpReponse.getEntity().getContentType();
Header encoding = httpReponse.getEntity().getContentEncoding();
InputStream responseInputStream = httpReponse.getEntity().getContent();
String contentTypeValue = null;
String encodingValue = null;
if (contentType != null) {
contentTypeValue = contentType.getValue();
}
if (encoding != null) {
encodingValue = encoding.getValue();
}
return new WebResourceResponse(contentTypeValue, encodingValue, responseInputStream);
} catch (ClientProtocolException e) {
//return null to tell WebView we failed to fetch it WebView should try again.
return null;
} catch (IOException e) {
//return null to tell WebView we failed to fetch it WebView should try again.
return null;
}
}
}
Webview wv = new WebView(this);
wv.setWebViewClient(wvc);
If your minimum API target is level 21, you can use the new shouldInterceptRequest which gives you additional request information (such as headers) instead of just the URL.
Upvotes: 44
Reputation: 491
As mentioned before, you can do this:
WebView host = (WebView)this.findViewById(R.id.webView);
String url = "<yoururladdress>";
Map <String, String> extraHeaders = new HashMap<String, String>();
extraHeaders.put("Authorization","Bearer");
host.loadUrl(url,extraHeaders);
I tested this and on with a MVC Controller that I extended the Authorize Attribute to inspect the header and the header is there.
Upvotes: 24
Reputation: 983
You should be able to control all your headers by skipping loadUrl and writing your own loadPage using Java's HttpURLConnection. Then use the webview's loadData to display the response.
There is no access to the headers which Google provides. They are in a JNI call, deep in the WebView source.
Upvotes: 3