When I use an expired PAT attempting to access the REST end-point, in the browser's Dev Tools console window I will get:
GET https://dev.azure.com/contoso/_apis/connectionData net::ERR_FAILED 401 (Unauthorized)
But it is impossible to get the fact that there's a 401 status code from inside JavaScript. I've tried fetch with "then" / "catch" / and await with try/catch. I've also tried XMLHttpRequest. The only error I get is:
TypeError: Failed to fetch
Since the browser is clearly seeing the 401 (Unauthorized)
, I'd like to be detect that status code.
The browser Dev Tools console also has the following:
Access to fetch at 'https://dev.azure.com/contoso/_apis/connectionData' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
The Network tab shows the 401 status as well:
Using no-cors
hides makes everything opaque, and you don't get the 401 status.
Here's a runnable sample:
<!DOCTYPE html>
<html>
<body>
<script>
(async function () {
"use strict";
const API_ORGANIZATION="contoso"
const expiredPat = 'ts44tplifj4hbbzeitcz5dg5az4q3mpqcxqmrxo4zwlgy3scobzq'
const pat = expiredPat
// Fetch data from Azure DevOps
async function ado(api) {
const url = `https://dev.azure.com/${API_ORGANIZATION}/_apis/${api}`
console.log(`url: ${url}`)
const response = await fetch(url, {
method: 'GET',
cache: 'no-cache',
mode: 'cors',
headers: {
Authorization: 'Basic ' + btoa(':' + pat),
Accept: 'application/json',
}
})
return await response.json()
}
// get the connectionData from Azure DevOps
async function getConnectionData() {
return await ado('connectionData')
}
function topText(text) {
var p = document.createElement('p');
p.innerHTML = text
document.body.prepend(p)
return p
}
// show the connection Data at the top of the window
async function showConnectionData() {
try {
const result = await getConnectionData();
topText(`Azure DevOps access authenticated as: ${result.authenticatedUser.providerDisplayName}`)
} catch(err) {
const p = topText(`${err} - See console`)
p.style.color = "red";
p.style.fontWeight = "999"
}
}
async function tryFetch() {
try {
await showConnectionData()
} catch(err) {
console.log(err);
}
}
document.addEventListener("DOMContentLoaded", function(event) {
tryFetch()
});
})();
</script>
</body>
</html>
The issue is caused by the CORS (Cross-Origin Resource Sharing).
According to this official document about Azure DevOps REST API:
Azure DevOps Services supports CORS, which enables JavaScript code served from a domain other than dev.azure.com/* to make Ajax requests to Azure DevOps Services REST APIs. Each request must provide credentials (PATs and OAuth access tokens are both supported options).
This means that Azure DevOps Services supports CORS when you provided the correct credentials. When the PAT is incorrect, the server responds with a 401 Unauthorized status and since the credentials are invalid, the server does not proceed with the request and does not include CORS headers.
For example, I tested the issue with the following script (replace the orgname and pat with the actual value):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Azure DevOps CORS Example</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<h1>Azure DevOps API Response</h1>
<div id="status"></div>
<div id="response"></div>
<script>
$(document).ready(function() {
$.ajax({
url: 'https://dev.azure.com/{orgname}/_apis/connectionData',
dataType: 'json',
headers: {
'Authorization': 'Basic ' + btoa("" + ":" + "pat")
}
}).done(function(results) {
$('#status').text('Status: 200 OK');
$('#response').text(JSON.stringify(results, null, 2));
}).fail(function(jqXHR, textStatus, errorThrown) {
$('#status').text('Status: ' + jqXHR.status + ' ' + textStatus);
$('#response').text('Response: ' + errorThrown);
});
});
</script>
</body>
</html>
When run the script with a correct PAT, you can see the Access-Control-Allow-Origin:*
in the response headers.
When run the script with an expired PAT, there is no Access-Control-Allow-Origin:*
in the response headers.
To workaround the CORS issue, you can refer this question for more details.
In my test, I tried to use the CORS Anywhere, a public demo of the CORS Anywhere service. It allows you to bypass the CORS restrictions when making requests to different domains. This should only be used for development purposes.
Steps:
https://cors-anywhere.herokuapp.com
to the URL in the script, which becomes to https://cors-anywhere.herokuapp.com/https://dev.azure.com/orgname/_apis/connectionData
Access-Control-Allow-Origin
in the response headers and get the 401 status code.