Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 34 additions & 23 deletions Sources/GraphQLVapor/HTTP/GraphQLHandler+HTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extension GraphQLHandler {
throw Abort(.methodNotAllowed, reason: "Mutations using GET are disallowed")
}
let context = try await computeContext(request)
let result = try await execute(
let result = await execute(
graphQLRequest: graphQLRequest,
context: context,
additionalValidationRules: config.additionalValidationRules
Expand All @@ -31,7 +31,7 @@ extension GraphQLHandler {
}
let graphQLRequest = try request.content.decode(GraphQLRequest.self)
let context = try await computeContext(request)
let result = try await execute(
let result = await execute(
graphQLRequest: graphQLRequest,
context: context,
additionalValidationRules: config.additionalValidationRules
Expand All @@ -43,7 +43,7 @@ extension GraphQLHandler {
graphQLRequest: GraphQLRequest,
context: Context,
additionalValidationRules: [@Sendable (ValidationContext) -> Visitor]
) async throws -> GraphQLResult {
) async -> GraphQLResult {
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#validation
let validationRules = GraphQL.specifiedRules + additionalValidationRules

Expand All @@ -59,9 +59,11 @@ extension GraphQLHandler {
operationName: graphQLRequest.operationName,
validationRules: validationRules
)
} catch {
} catch let error as GraphQLError {
// This indicates a request parsing error
throw Abort(.badRequest, reason: error.localizedDescription)
return GraphQLResult(data: nil, errors: [error])
} catch {
return GraphQLResult(data: nil, errors: [GraphQLError(message: error.localizedDescription)])
}
return result
}
Expand All @@ -74,36 +76,45 @@ extension GraphQLHandler {

let response = Response()

let configuredMediaTypes: Set<HTTPMediaType> = [.jsonGraphQL, .json]

// Try to respond with the best matching media type, in order
var selectedMediaType: HTTPMediaType? = nil
for mediaType in headers.accept.mediaTypes {
do {
try response.content.encode(result, as: mediaType)
return response
} catch {
continue
if configuredMediaTypes.contains(mediaType) {
selectedMediaType = mediaType
break
}
}

// If no exact matches, look for any matching wildcards
let acceptableMediaSet = HTTPMediaTypeSet(mediaTypes: headers.accept.mediaTypes)
for mediaType: HTTPMediaType in [.jsonGraphQL, .json] {
if acceptableMediaSet.contains(mediaType) {
do {
try response.content.encode(result, as: mediaType)
return response
} catch {
continue
if selectedMediaType == nil {
let acceptableMediaSet = HTTPMediaTypeSet(mediaTypes: headers.accept.mediaTypes)
for mediaType in configuredMediaTypes {
if acceptableMediaSet.contains(mediaType) {
selectedMediaType = mediaType
break
}
}
}

// Use the default if configured to do so
if config.allowMissingAcceptHeader {
try response.content.encode(result, as: .jsonGraphQL)
return response
if selectedMediaType == nil, config.allowMissingAcceptHeader {
selectedMediaType = .jsonGraphQL
}

guard let selectedMediaType else {
// Fail
throw Abort(.notAcceptable)
}

if selectedMediaType == .jsonGraphQL, result.data == nil {
// We must return `bad request` with the content if there were failures preventing a partial result
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#applicationgraphql-responsejson
response.status = .badRequest
}

// Fail
throw Abort(.notAcceptable)
try response.content.encode(result, as: selectedMediaType)
return response
}
}
167 changes: 167 additions & 0 deletions Tests/GraphQLVaporTests/HTTPStatusCodeGraphQLJSONTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import Foundation
import GraphQL
import GraphQLTransportWS
@testable import GraphQLVapor
import GraphQLWS
import NIOFoundationCompat
import Testing
import Vapor
import VaporTesting

/// Validates status code behavior for the `application/graphql-response+json` media type.
///
/// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#applicationgraphql-responsejson
@Suite
struct HTTPStatusCodeGraphQLJSONTests {
@Test func parsingFailureGivesBadRequest() async throws {
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#json-parsing-failure-1
try await withApp { app in
app.graphql(schema: helloWorldSchema) { _ in
EmptyContext()
}

try await app.test(.POST, "/graphql", headers: jsonGraphQLHeaders) { req in
try req.content.encode(#"{"query":"#)
} afterResponse: { response in
#expect(response.status == .badRequest)
}
}
}

@Test func invalidParametersGivesBadRequest() async throws {
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#invalid-parameters-1
try await withApp { app in
app.graphql(schema: helloWorldSchema) { _ in
EmptyContext()
}

try await app.test(.POST, "/graphql", headers: jsonGraphQLHeaders) { req in
try req.content.encode(["qeury": "{__typename}"])
} afterResponse: { response in
#expect(response.status == .badRequest)
}
}
}

@Test func documentParsingFailureGivesBadRequest() async throws {
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#document-parsing-failure-1
try await withApp { app in
app.graphql(schema: helloWorldSchema) { _ in
EmptyContext()
}

try await app.test(.POST, "/graphql", headers: jsonGraphQLHeaders) { req in
try req.content.encode(GraphQLRequest(
query: "{"
))
} afterResponse: { response in
#expect(response.status == .badRequest)
}
}
}

@Test func documentValidationFailureGivesBadRequest() async throws {
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#document-validation-failure-1
try await withApp { app in
app.graphql(schema: helloWorldSchema) { _ in
EmptyContext()
}

try await app.test(.POST, "/graphql", headers: jsonGraphQLHeaders) { req in
// Fails "No Unused Variables" validation rule
try req.content.encode(GraphQLRequest(
query: "query A($name: String) { hello }"
))
} afterResponse: { response in
#expect(response.status == .badRequest)
}
}
}

@Test func operationCannotBeDeterminedGivesBadRequest() async throws {
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#operation-cannot-be-determined-1
try await withApp { app in
app.graphql(schema: helloWorldSchema) { _ in
EmptyContext()
}

try await app.test(.POST, "/graphql", headers: jsonGraphQLHeaders) { req in
try req.content.encode(GraphQLRequest(
query: "abc { hello }"
))
} afterResponse: { response in
#expect(response.status == .badRequest)
}
}
}

@Test func variableCoercionFailureGivesBadRequest() async throws {
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#variable-coercion-failure-1
try await withApp { app in
let schema = try GraphQLSchema(
query: GraphQLObjectType(
name: "Query",
fields: [
"get": GraphQLField(
type: GraphQLString,
args: [
"name": GraphQLArgument(type: GraphQLString),
],
resolve: { _, args, _, _ in
guard let name = args["name"].string else {
throw GraphQLError(message: "Name arg is required")
}
return name
}
),
]
)
)
app.graphql(schema: schema) { _ in
EmptyContext()
}

try await app.test(.POST, "/graphql", headers: jsonGraphQLHeaders) { req in
try req.content.encode(GraphQLRequest(
query: "query getName($name: String!) { get(name: $name) }",
variables: ["name": .null]
))
} afterResponse: { response in
#expect(response.status == .badRequest)
}
}
}

@Test func fieldErrorGivesOk() async throws {
// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#field-errors-encountered-during-execution-1
try await withApp { app in
let schema = try GraphQLSchema(
query: GraphQLObjectType(
name: "Query",
fields: [
"error": GraphQLField(
type: GraphQLString,
resolve: { _, _, _, _ in
throw GraphQLError(message: "Something went wrong")
}
),
]
)
)
app.graphql(schema: schema) { _ in
EmptyContext()
}

try await app.test(.POST, "/graphql", headers: jsonGraphQLHeaders) { req in
try req.content.encode(GraphQLRequest(query: "{ error }"))
} afterResponse: { response in
#expect(response.status == .ok)
#expect(response.headers.contentType == .jsonGraphQL)

let response = try response.content.decode(GraphQLResult.self)
#expect(!response.errors.isEmpty)
#expect(response.errors.first?.message == "Something went wrong")
}
}
}
}
Loading