I'm using the Github API to store json documents as Gists. Following the API as best I can, I get a 422 error. This indicates that there's nothing wrong with the request, but it's not being processed. Hints elsewhere indicate that this can be because of server-side specific processing rules, but I don't know what they would be.
A sample of the JSON I'm sending as the request is here: https://gist.github.com/urbanistica/fa2cab822cc7ce6a40f6b9394f811075
My Auth token is up to date, and I don't think that can have anything to do with it, since I would not be getting this error. The Github API docs for creating a Gist are here: https://docs.github.com/en/rest/gists/gists#create-a-gist, and from what I can determine, the JSON I'm sending agrees with this. The JSON has been checked for validity on JSONLint and in ThunderClient. The headers are set as recommended. I'm nowhere near the rate limiter, and I don't have other information that seems relevant to why this doesn't work....
This is the code:
struct GistPayload : Codable {
var Public : Bool
var description : String
var files : Dictionary<String, Dictionary<String, ProjectExport>>
}
@objc func exportProject() {
guard let projectExport = userProject?.export() else { return }
let src = GistPayload(Public: true, description: "testing Gist upload", files: ["testGist" : ["content" : projectExport]])
sendToGist(payload: src)
}
func encode(src : GistPayload) -> String? {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(src)
return String(data: data, encoding: .utf8)!
} catch {print("problem \(error)")}
return nil
}
func sendToGist(payload : GistPayload) {
guard var data = encode(src: payload) else {
print("no data")
return
}
data = data.replacingOccurrences(of: "Public", with: "public")
let url = URL(string: "https://api.github.com/gists")!
var request = URLRequest(url: url)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
request.setValue("Bearer \(StartController.token)", forHTTPHeaderField: "Authorization")
request.httpMethod = "POST"
request.httpBody = data.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard
let data = data,
let response = response as? HTTPURLResponse, error == nil
else {
print("error", error ?? URLError(.badServerResponse))
return
}
guard (200 ... 299) ~= response.statusCode else {
print("statusCode should be 2xx, but is \(response.statusCode)")
print("response = \(response)")
return
}
do {
print("done")
} catch {
print(error) // parsing error
if let responseString = String(data: data, encoding: .utf8) {
print("responseString = \(responseString)")
} else {
print("unable to parse response as string")
}
}
}
task.resume()
}
The Docs say that 422 indicates, "Validation failed, or the endpoint has been spammed." I'm me, and I don't abuse Github, if this indicates the problem, how is this flag raised, and how can I avoid it?
EDIT: Using Thunderclient, I can create the gists via the API. I'm using JSON sent to the server, so the JSON conforms.
I've updated the code to this:
struct GistData {
var filename : String
var description : String
var publc : Bool
var payload : ProjectExport
func files() -> Dictionary<String, Dictionary<String, ProjectExport>> {
var rslt : Dictionary<String, Dictionary<String, ProjectExport>>
return [filename : ["content" : payload]]
}
func output() -> Output {
return Output(description: description, Public: publc, files: files())
}
struct Output : Codable {
var description : String
var Public : Bool
var files : Dictionary<String, Dictionary<String, ProjectExport>>
}
}
@objc func exportProject() {
guard let projectExport = userProject?.export() else { return }
var gistData = GistData(filename: "newThingie.json", description: "a thing", publc: true, payload: projectExport)
sendToGist(payload: gistData.output())
}
func encode(src : GistData.Output) -> String? {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(src)
return String(data: data, encoding: .utf8)!
} catch {print("problem \(error)")}
return nil
}
and this is (still) the response:
statusCode should be 2xx, but is 422
response = <NSHTTPURLResponse: 0x600001d48d40> { URL: https://api.github.com/gists } { Status Code: 422, Headers { "Access-Control-Allow-Origin" = ( "*" ); "Content-Length" = ( 38981 ); "Content-Type" = ( "application/json; charset=utf-8" ); Date = ( "Tue, 22 Nov 2022 04:10:48 GMT" ); Server = ( "GitHub.com" ); "Strict-Transport-Security" = ( "max-age=31536000; includeSubdomains; preload" ); Vary = ( "Accept-Encoding, Accept, X-Requested-With" ); "access-control-expose-headers" = ( "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset" ); "content-security-policy" = ( "default-src 'none'" ); "github-authentication-token-expiration" = ( "2023-02-20 03:06:05 UTC" ); "referrer-policy" = ( "origin-when-cross-origin, strict-origin-when-cross-origin" ); "x-accepted-oauth-scopes" = ( "" ); "x-content-type-options" = ( nosniff ); "x-frame-options" = ( deny ); "x-github-media-type" = ( "github.v3; format=json" ); "x-github-request-id" = ( "D032:5EB9:94899C:9949C2:637C4BC8" ); "x-oauth-scopes" = ( gist ); "x-ratelimit-limit" = ( 5000 ); "x-ratelimit-remaining" = ( 4992 ); "x-ratelimit-reset" = ( 1669090408 ); "x-ratelimit-resource" = ( core ); "x-ratelimit-used" = ( 8 ); "x-xss-protection" = ( 0 ); } }
EDIT AND SOLUTION: I'm including the working code here:
struct Tester : Codable {
var description : String
var publc : Bool
var filename : String
var payload : String
func encodePayload() {
}
func files() -> Dictionary<String, Dictionary<String, String>> {
return [filename : ["content" : payload]]
}
func output() -> Output {
return Output(description: description, Public: publc, files: files())
}
struct Output : Codable {
var description : String
var Public : Bool
var files : Dictionary<String, Dictionary<String, String>>
}
}
@objc func exportProject() {
guard let projectExport = userProject?.export() else { return }
guard var encodedData = encode(src: projectExport) else {
print("problem encoding data")
return
}
var gistTest = Tester(description: "please work", publc: true, filename: "justMaybe.md", payload: encodedData)
sendToGist(payload: gistTest.output())
}
func encode(src : ProjectExport) -> String? {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(src)
return String(data: data, encoding: .utf8)!
} catch {print("problem \(error)")}
return nil
}
func encode(src : Tester.Output) -> String? {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(src)
return String(data: data, encoding: .utf8)!
} catch {print("problem \(error)")}
return nil
}
func sendToGist(payload : Tester.Output) {
guard var data = encode(src: payload) else {
print("no data")
return
}
let url = URL(string: "https://api.github.com/gists")!
var request = URLRequest(url: url)
request.httpBody = data.data(using: String.Encoding.utf8)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
request.setValue("Bearer \(StartController.token)", forHTTPHeaderField: "Authorization")
request.httpMethod = "POST"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard
let data = data,
let response = response as? HTTPURLResponse, error == nil
else { // check for fundamental networking error
print("error", error ?? URLError(.badServerResponse))
return
}
guard (200 ... 299) ~= response.statusCode else { // check for http errors
print("statusCode should be 2xx, but is \(response.statusCode)")
print("response = \(response)")
return
}
do {
print("done")
} catch {
print(error) // parsing error
if let responseString = String(data: data, encoding: .utf8) {
print("responseString = \(responseString)")
} else {
print("unable to parse response as string")
}
}
}
task.resume()
}