呼叫API時,content-type與Model Binding的關係

header的content-type紀錄header body的資料格式,後端API可以依據content-type知道header body內的資料格式,再來解析header body資料,進行Model Binding.

呼叫POST API時,不管是透過下列何種方式

  • HTML傳統表單
  • JQuery
  • Angular的HttpClientModule
  • 後端程式直接呼叫(ex: .Net MVC)

遇到POST要傳遞參數時,後端程式會收到的header body資料是什麼格式,是取決於前端如何提交資料(content-type)的。

用戶端提交方式MVC 接收到的 Content-Type
一般 HTML 表單application/x-www-form-urlencoded
表單含檔案 (enctype="multipart/form-data")multipart/form-data
AJAXapplication/x-www-form-urlencoded

用 HTML <form>提交資料(預設content-type:application/x-www-form-urlencoded)

模擬POST 表單資料到後端
再用API測試軟體模擬POST 表單資料到後端。如先前所說, HTML 表單預設的content-type為application/x-www-form-urlencoded。
<!--如果不透過api測試軟體,form表單大概長這樣-->
<form method="post" action="/Home/Submit">
   <input type="text" name="id" />
   <input type="text" name="username" />
   <input type="submit" value="Submit">
</form>

 

content-type與後端資料來源的屬性的對應

上面結果我們可以發現,我們收到一個415 Unsupported Media Type的回應,來分析一下出了什麼問題。先來看一下我們後端的API。

        public async Task<Result<string>> TestAPI(ApiData data)
        {
            var result = new Result<string>();
            try
            {
                result.Content = $"{data.id}-{data.name}";
                result.Success = true ;
            }
            catch (Exception ex)
            {
                result.Message = ex.Message;
            }            
            return result;
        }
很快就發現原因了,在 .NET WebAPI 中,.Net預設會用[FromBody]把header body內的資料格式當作JSON來解析。由於我們前端給的content-type格式為x-www-form-urlencoded,並不是application/json。後端Controller無法把header body資料當作JSON格式來解析,導致回應415 Error。
PS:如果後端沒有再標註參數來源屬性[FromXXXX]的話,.Net 預設是使用[FromBody],把header body內的資料格式當作JSON來解析。所以當前端content-type為application/json時,[FromBody]加不加都可以。

知道問題的原因後,我們在參數前面加上[FromForm]的資料來源屬性,讓.Net知道header body的資料格式為x-www-form-urlencoded,就能正常取得前端POST到後端的參數資料了。

        public async Task<Result<string>> TestAPI([FromForm]ApiData data)
        {
            var result = new Result<string>();
            try
            {
                result.Content = $"{data.id}-{data.name}";
                result.Success = true ;
            }
            catch (Exception ex)
            {
                result.Message = ex.Message;
            }            
            return result;
        }
表單如果包含上傳檔案
form表單如果有包含檔案的話,必須要再
加上enctype="multipart/form-data"。content-type也會由原本的x-www-form-urlencoded變成multipart/form-data。
<form method="post" enctype="multipart/form-data">
    <input name="Title" />
    <input name="File" type="file" />
</form>

Angular的HttpClientModule(預設content-type:依據body內容自行決定)

下面是用ChatGPT查詢出來的資料(Prompt:angular httpclient 預設的content type是什麼):

Angular 的 HttpClient 中,預設的 Content-Type 會根據你送出的資料類型而自動決定。以下是常見情況:

傳送的資料類型預設 Content-Type
JavaScript 物件 {}(例如 JSON 資料)application/json
FormData(如圖片上傳)無設定(瀏覽器自動設定為 multipart/form-data,含 boundary)
字串或 URLSearchParamsapplication/x-www-form-urlencoded
BlobArrayBuffer 等原始資料無設定(需自行指定)

 

Body內容為物件

這邊寫一個呼叫POST API的TypeScript Function。我們body內用是一個{ }物件

  TestAPI(): Observable<IResultDto<string>>
  {
    const url = `${environment.apiBaseUrl}/DesignAuditRise/TestAPI`;
    const options = this.generatePostOptions();
    const data: any = {
      id: 123,
      username: 'jojo999'
    }
    return this.httpClient.post<IResultDto<string>>(url, data, options);
  }

後端程式一樣用先前的TestAPI

        public async Task<Result<string>> TestAPI(ApiData data)
        {
            var result = new Result<string>();
            try
            {
                result.Content = $"{data.id}-{data.username}";
                result.Success = true ;
            }
            catch (Exception ex)
            {
                result.Message = ex.Message;
            }            
            return result;
        }

Angular把body資料格式自動判斷為application/json

如先前所說,後端API預設會使用[FromBody]把header body資料格式判斷為JSON來解析。application/json與[FromBody]是可以匹配的,正常Model Binding並取得API回傳值

Body內容為FormData
  TestAPI(): Observable<IResultDto<string>>
  {
    const url = `${environment.apiBaseUrl}/DesignAuditRise/TestAPI`;
    const options = this.generatePostOptions();

    const data = new FormData();
    data.append('id', '123');
    data.append('username', 'jojo9988');
    
    return this.httpClient.post<IResultDto<string>>(url, data, options);
  }

接著直接呼叫API,Angular把body資料格式自動判斷為application/json。但卻得到了一個415 Error。問題原因跟上面範例一樣,multipart/form-data與預設使用的[FromBody]不能匹配,所以回傳415 Error。

後端API在Model前加上[FromForm],讓.Net知道header body資料格式為multipart/form-data。接著在戳一次API,就能成功Model Binding取得API回傳值了。

        public async Task<Result<string>> TestAPI([FromForm]ApiData data)
        {
            var result = new Result<string>();
            try
            {
                result.Content = $"{data.id}-{data.username}";
                result.Success = true ;
            }
            catch (Exception ex)
            {
                result.Message = ex.Message;
            }            
            return result;
        }

 

透過上面兩個測試範例,可以知道在Angular透過HttpClient呼叫POST API時,會自己依照body內容的型別來判斷要使用的content-type。


透過前端AJAX呼叫API(預設content-type:application/x-www-form-urlencoded)

後端API 如果是使用.NET(Core) WebAPI專案

後端API程式如下

public async Task<Result<string>> TestAPI(ApiData data)
{
	var result = new Result<string>();
	try
	{
		result.Content = $"{data.id}-{data.name}";
		result.Success = true ;
	}
	catch (Exception ex)
	{
		result.Message = ex.Message;
	}            
	return result;
}

我們這裡寫一個AJAX來呼叫後端的測試API,並且先不指定content-type

$.ajax({
	type: "POST",
	url: "https://localhost:44371/api/DesignAuditRise/TestAPI",

	data: { id: 111, username: "jojoaaa" },   
	dataType: "json",
    headers: {
        'Access-Control-Allow-Origin': '*',
    },
});
結果如下,得到了一個415的回應。從下面兩張圖,我們可以得知AJAX如果不加上content-type: application/json的話,預設是使用application/x-www-form-urlencoded來當作header body資料的格式,但後端API預設用的是[FromBody]來解析JSON格式,所以會得到415的錯誤。

要得到正確的API回應的話,我們可以在後端參數API加上[FromForm]。讓.Net知道header body的資料格式為application/x-www-form-urlencoded,這樣就可以正常取得API回傳資料了。

public async Task<Result<string>> TestAPI([FromForm]ApiData data)//加上[FromForm]讓.Net知道header body的資料格式為application/x-www-form-urlencoded
{
	var result = new Result<string>();
	try
	{
		result.Content = $"{data.id}-{data.username}";
		result.Success = true ;
	}
	catch (Exception ex)
	{
		result.Message = ex.Message;
	}            
	return result;
}

我們也可以修改AJAX參數的格式為JSON,並加上contentType: "application/json"。讓.Net知道header body的資料格式為application/json,這樣也是可以正常取得API回傳資料。

$.ajax({
	type: "POST",
	url: "https://localhost:44371/api/DesignAuditRise/TestAPI",

	data: JSON.stringify({ id: 111, username: "jojoaaa" }),   
	contentType: "application/json"
	dataType: "json",
    headers: {
        'Access-Control-Allow-Origin': '*',
    },
});
public async Task<Result<string>> TestAPI(ApiData data)//[FromBody]可加可不加,.Net預設就是用FromBody來解析JSON做參數binding.
{
	var result = new Result<string>();
	try
	{
		result.Content = $"{data.id}-{data.username}";
		result.Success = true ;
	}
	catch (Exception ex)
	{
		result.Message = ex.Message;
	}            
	return result;
}

 

後端API 如果是使用.NET Framework MVC專案
先前有提到.Net預設會用[FromBody]把header body資料用JSON進行解析,,那如果後端API使用的是.NET Framework MVC開發的話,預設會把header body內容的格式當成application/x-www-form-urlencoded來解析。

後端API程式如下,這裡我們沒指定預設要用什麼格式來解析header body內容。來測試看看.Net Framework MVC預設會使用何種格式來解析。

[HttpPost]
public ActionResult TestAPI(ApiData data)
{
	var res = new PCBRepairNavi.Models.ResultNew<string>()
	{
		Content = $"{data.id}-{data.username}",
		Success = true
	};
	return Content(JsonConvert.SerializeObject(res), "application/json");
}

我們這裡寫一個AJAX來呼叫後端的測試API,並且先不指定content-type

function TestAPI() {
    $.ajax({
        type: "post",
        url: testApiUrl,
        data:{ id: 123 , username: 'jojo123'},
        dataType: "json"
    }).done(function(data) {
        console.log(data);
    })
}
可以看到AJAX預設使用的content-type為application/x-www-form-urlencoded。

也成功取得API的回應

接著修改一下AJAX程式,並指定content-type為application/json

function TestAPI() {
    $.ajax({
        type: "post",
        url: testApiUrl,
        data: JSON.stringfy({ id: 123 , username: 'jojo123'}),
        contentType: "application/json"
        dataType: "json"
    }).done(function(data) {
        console.log(data);
    })
}

request header裡面的content-type為application/json。

同樣的也成功取得API的回應

看完上面測試,可能會覺得.Net Framework MVC不需要像.Net一樣要另外在後端指定資料來源格式([FromXXXXX]),也可以自己透過使用者給的content-type來解析header body內容。實際上是這樣,但也不是這樣,.Net Framework MVC也是有預設的解析格式為application/x-www-form-urlencoded,那為什麼我們不必在Controller參數加上來源格式卻也能正常解析application/json的資料呢?因為在System.Web.Mvc底層程式裡面有一個ValueProviderFactories來幫我們處理Model binding(詳細請參考這篇文章:ASP.NET MVC 開發心得分享 (25):ModelBinder 與 ValueProvider 的用途),會依照header body內的資料內容來判斷該透過何種格式來解析資料。

 

後端API 如果是使用.NET Framework WebAPI專案
如果API是使用.NET Framework WebAPI來開發的話,那就可以跟之前一樣使用[FromBody]來讓後端API知道header body的內容要使用JSON來解析。但.NET Framework MVC底層因為沒有引用System.Web.Http,所以是沒有[FromBody]這個屬性的,大家別搞錯了!!但經過測試,在.NET Framework WebAPI專案裡,也只有[FromBody][FromUri]這兩個屬性可以使用。

在.NET Framework MVC專案的Controller使用[FromBody]會報錯

在.NET Framework WebAPI專案的Controller使用[FromBody]是不會報錯的


後端程式呼叫外部API

透過這種方式呼叫API,content-type的設定要特別注意,要確定api接收的參數格式為何,再來指定httpRequestMessage.Content(FormUrlEncodedContent即為x-www-form-urlencoded),如果格式沒有選對的話,API是永遠打不通的!
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, myURL);


httpRequestMessage.Content = new FormUrlEncodedContent(form);
HttpResponseMessage tokenResponse = httpClient.SendAsync(httpRequestMessage).Result;

Ref: