Rory McCrossan Rory McCrossan - 1 month ago 7
ASP.NET (C#) Question

Unable to set contentType when uploading Azure Blob via REST API

I have the below code which successfully uploads an image to my Azure Blob storage container. However, by default it sets the

ContentType
of the stored file to
application/octet-stream
I would like to change this to
image/jpg
.

To do that I wrote the lines which are in comments below. They set the required content-type headers as far as I understand them from the Azure documentation, however the request now results in a
403 Unauthorized
response instead of a
200
.

private static void PutBlob(string filenameToSave)
{
var requestMethod = "PUT";
var urlPath = _storageContainer + "/" + filenameToSave;
var storageServiceVersion = "2015-12-11";
var date = DateTime.UtcNow.ToString("R", CultureInfo.InvariantCulture);
var blobType = "BlockBlob";
var imageBytes = GetImageBytes();
var canonicalizedResource = "/" + _storageAccount + "/" + urlPath;

// DOESN'T WORK:
var canonicalizedHeaders = "x-ms-blob-type:" + blobType + "\nx-ms-blob-content-type:image/jpeg\nx-ms-date:" + date + "\nx-ms-version:" + storageServiceVersion;
// WORKS:
//var canonicalizedHeaders = "x-ms-blob-type:" + blobType + "\nx-ms-date:" + date + "\nx-ms-version:" + storageServiceVersion;

// DOESN'T WORK:
string stringToSign = requestMethod + "\nimage/jpeg\n\n" + imageBytes.Length + "\n\n\n\n\n\n\n\n\n" + canonicalizedHeaders + "\n" + canonicalizedResource;
// WORKS:
//string stringToSign = requestMethod + "\n\n\n" + imageBytes.Length + "\n\n\n\n\n\n\n\n\n" + canonicalizedHeaders + "\n" + canonicalizedResource;

string authorizationHeader = GenerateSharedKey(stringToSign, _storageKey, _storageAccount);

var uri = $"https://{_storageAccount}.blob.core.windows.net/{urlPath}";
var request = (HttpWebRequest)WebRequest.Create(uri);
request.Method = requestMethod;
request.ContentType = "image/jpeg"; // DOESN'T WORK

request.Headers.Add("x-ms-blob-content-type", "image/jpeg"); // DOESN'T WORK
request.Headers.Add("x-ms-blob-type", blobType);
request.Headers.Add("x-ms-date", date);
request.Headers.Add("x-ms-version", storageServiceVersion);
request.Headers.Add("Authorization", authorizationHeader);

var stream = request.GetRequestStream();
stream.Write(imageBytes, 0, imageBytes.Length);

using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
// check the response status here, 200/201 means it worked

Console.WriteLine("File uploaded");
}
}

private static string GenerateSharedKey(string stringToSign, string key, string account)
{
string signature;
var unicodeKey = Convert.FromBase64String(key);
using (var hmacSha256 = new HMACSHA256(unicodeKey))
{
var dataToHmac = Encoding.UTF8.GetBytes(stringToSign);
signature = Convert.ToBase64String(hmacSha256.ComputeHash(dataToHmac));
}
return string.Format(CultureInfo.InvariantCulture, "{0} {1}:{2}", "SharedKey", account, signature);
}


I can see that the issue is with the generated
authorizationHeader
, but I cannot see why as I have followed the guide. Can anyone help me shed some light on this.

Also note that I have to do this through the REST API and not the Microsoft.WindowsAzure.Storage library (annoyingly, as I know it's a fraction of the code when done in that way).

Thanks.

Answer

I discovered one issue in your code:

// DOESN'T WORK:
    string stringToSign = requestMethod + "\nimage/jpeg\n\n" + imageBytes.Length + "\n\n\n\n\n\n\n\n\n" + canonicalizedHeaders + "\n" + canonicalizedResource;

Essentially ContentType should not be the 2nd parameter. 2nd parameter is Content-Encoding based on the documentation

StringToSign = VERB + "\n" +
               Content-Encoding + "\n" +
               Content-Language + "\n" +
               Content-Length + "\n" +
               Content-MD5 + "\n" +
               Content-Type + "\n" +
               Date + "\n" +
               If-Modified-Since + "\n" +
               If-Match + "\n" +
               If-None-Match + "\n" +
               If-Unmodified-Since + "\n" +
               Range + "\n" +
               CanonicalizedHeaders + 
               CanonicalizedResource;

So once you change this, your code should work:

        string stringToSign = requestMethod + "\n" +
            "\n" + //Content Encoding
            "\n" + //Content Language
            imageBytes.Length + "\n" + //Content Length
            "\n" + //Content MD5
            "image/jpeg" + "\n" + //Content Type
            "\n" + //Date
            "\n" + //If - Modified - Since
            "\n" + //If - Match
            "\n" + //If - None - Match
            "\n" + //If - Unmodified - Since
            "\n" + //Range +
           canonicalizedHeaders + "\n" +
           canonicalizedResource;

Also, you don't need to specify both Content-Type and x-ms-blob-content-type. If you define x-ms-blob-content-type, then it should be included in canonicalizedHeaders.

Here's the code I used to test this:

    private static void PutBlob(string filenameToSave)
    {
        var requestMethod = "PUT";
        var urlPath = "<container-name>" + "/" + filenameToSave;
        var storageServiceVersion = "2015-12-11";
        var date = DateTime.UtcNow.ToString("R", CultureInfo.InvariantCulture);
        var blobType = "BlockBlob";
        var imageBytes = File.ReadAllBytes(@"File Path");
        var canonicalizedResource = "/" + accountName + "/" + urlPath;

        // DOESN'T WORK:
        //var canonicalizedHeaders = "x-ms-blob-type:" + blobType + "\nx-ms-blob-content-type:image/jpeg\nx-ms-date:" + date + "\nx-ms-version:" + storageServiceVersion;
        // WORKS:
        var canonicalizedHeaders = "x-ms-blob-type:" + blobType + "\nx-ms-date:" + date + "\nx-ms-version:" + storageServiceVersion + "\n";

        // DOESN'T WORK:
        //string stringToSign = requestMethod + "\nimage/jpeg\n\n" + imageBytes.Length + "\n\n\n\n\n\n\n\n\n" + canonicalizedHeaders + "\n" + canonicalizedResource;
        // WORKS:
        //string stringToSign = requestMethod + "\n\n\n" + imageBytes.Length + "\n\n\n\n\n\n\n\n\n" + canonicalizedHeaders + "\n" + canonicalizedResource;
        string stringToSign = requestMethod + "\n" +
            "\n" + //Content Encoding
            "\n" + //Content Language
            imageBytes.Length + "\n" + //Content Length
            "\n" + //Content MD5
            "image/jpeg" + "\n" + //Content Type
            "\n" + //Date
            "\n" + //If - Modified - Since
            "\n" + //If - Match
            "\n" + //If - None - Match
            "\n" + //If - Unmodified - Since
            "\n" + //Range +
           canonicalizedHeaders +
           canonicalizedResource;
        string authorizationHeader = GenerateSharedKey(stringToSign, accountKey, accountName);

        var uri = "https://" + accountName + ".blob.core.windows.net/" + urlPath;

        var request = (HttpWebRequest)WebRequest.Create(uri);
        request.Method = requestMethod;
        request.ContentType = "image/jpeg"; // DOESN'T WORK

        //request.Headers.Add("x-ms-blob-content-type", "image/jpeg"); // DOESN'T WORK
        request.Headers.Add("x-ms-blob-type", blobType);
        request.Headers.Add("x-ms-date", date);
        request.Headers.Add("x-ms-version", storageServiceVersion);
        request.Headers.Add("Authorization", authorizationHeader);

        var stream = request.GetRequestStream();
        stream.Write(imageBytes, 0, imageBytes.Length);

        using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
        {
            // check the response status here, 200/201 means it worked

            Console.WriteLine("File uploaded");
        }
    }