URLRequest with its httpMethod property set to "POST". Define the appropriate Content-Type header (such as application/json or application/x-www-form-urlencoded), assign your encoded Data to request.httpBody, and execute the request using URLSession.shared.data(for: request).Why this happens
By default, an initialized URLRequest in Foundation uses the GET method and contains no payload body. When communicating with web services or APIs that expect form submissions or database modifications, sending a plain URL or unconfigured URLRequest results in a GET query or an empty payload error on the server (frequently returning HTTP 400 or HTTP 405 status codes).
Additionally, web servers require incoming payloads to match the format declared in the request headers. If you send JSON data without specifying Content-Type: application/json, or send raw parameter strings without percent-encoding for application/x-www-form-urlencoded, the server cannot parse the parameters.
To transmit parameters successfully, you must explicitly set the HTTP method, supply the correct Content-Type header, and serialize your payload into Data before assigning it to request.httpBody.
How to fix it
1. Send JSON payloads using modern async/await (Recommended)
Most modern REST APIs expect JSON. Use Swift’s Encodable protocol with JSONEncoder and URLSession.shared.data(for:) to send a typed POST request.
import Foundation
struct UserPayload: Encodable {
let id: Int
let name: String
}
func sendUserPostRequest() async throws {
guard let url = URL(string: "https://example.com/api/users") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
let payload = UserPayload(id: 13, name: "Jack")
request.httpBody = try JSONEncoder().encode(payload)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
print("Success! Received \(data.count) bytes.")
}
2. Send x-www-form-urlencoded parameters (Standard PHP forms)
Traditional endpoints, such as standard PHP scripts reading $_POST, expect application/x-www-form-urlencoded data rather than JSON. Special characters (including spaces, ampersands, and slashes) must be percent-encoded.
import Foundation
extension CharacterSet {
static let urlQueryValueAllowed: CharacterSet = {
let generalDelimitersToEncode = ":#[]@"
let subDelimitersToEncode = "!$&'()*+,;="
var allowed: CharacterSet = .urlQueryAllowed
allowed.remove(charactersIn: generalDelimitersToEncode + subDelimitersToEncode)
return allowed
}()
}
extension Dictionary where Key == String, Value: Any {
func percentEncoded() -> Data? {
map { key, value in
let escapedKey = key.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
return "\(escapedKey)=\(escapedValue)"
}
.joined(separator: "&")
.data(using: .utf8)
}
}
func sendFormPostRequest() async throws {
guard let url = URL(string: "https://example.com/postName.php") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
let parameters: [String: Any] = [
"id": 13,
"name": "Jack & Jill"
]
request.httpBody = parameters.percentEncoded()
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
if let responseString = String(data: data, encoding: .utf8) {
print("Server response: \(responseString)")
}
}
3. Send a POST request using traditional completion handlers (dataTask)
If maintaining a codebase that does not yet use structured concurrency, construct the request with URLSessionDataTask:
import Foundation
func sendLegacyPostRequest(completion: @escaping (Result<Data, Error>) -> Void) {
guard let url = URL(string: "https://example.com/api/users") else {
completion(.failure(URLError(.badURL)))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let parameters: [String: Any] = ["id": 13, "name": "Jack"]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
} catch {
completion(.failure(error))
return
}
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode),
let data = data else {
completion(.failure(URLError(.badServerResponse)))
return
}
completion(.success(data))
}
task.resume()
}
How to prevent it
- Check server content expectations: Confirm whether your backend requires
application/jsonorapplication/x-www-form-urlencodedbefore building the request body. - Always inspect HTTP status codes: Cast
responsetoHTTPURLResponseand check for a200...299status code; network transport can succeed even if the server returns a4xxor5xxerror. - Use strongly-typed models: Prefer Swift structs conforming to
Codableover untyped[String: Any]dictionaries to eliminate runtime serialization failures. - Resume data tasks: When working with completion-based
URLSessionDataTask, ensure you invoketask.resume(). The task will never fire without this call.
FAQ
What happens if I forget to set the Content-Type header?
Most backend frameworks will either fail to decode the payload or drop incoming parameters into an empty request body. Explicitly setting Content-Type informs the server parser how to read httpBody.
Can I send form parameters in the URL query string for a POST request?
While some servers accept query parameters on POST URLs, standard REST and HTTP conventions require passing data in the httpBody. Putting parameters in the body avoids URL length limitations and keeps sensitive data out of server access logs.
Why does my request succeed but the server reports empty parameters?
This usually occurs when sending JSON data to an endpoint expecting standard form data (x-www-form-urlencoded), or vice versa. Ensure your Content-Type header strictly matches the serialization format of request.httpBody.
Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.
