TSOA Fallstricke: HTTP Methoden und Übergabeparameter (Deutsch)

TSOA Fallstricke: HTTP Methoden und Übergabeparameter (Deutsch)

Dies ist der dritte Teil einer Serie von Beiträgen zum Thema Fallstricke beim Einsatz von TSOA. Dabei geht es um Lösungen von Schwierigkeiten die im praktischen Einsatz dieses Frameworks auftreten. In diesem Beitrag geht es darum welche Parameter und Rückgabe Datentypen mit welchen HTTP Methoden (GET, PUT, POSt, DELETE) und TSOA kompatibel. Auch bei Verwendung von Feldern und optionalen Parametern sind einige Besonderheiten zu beachten.


HTTP Methoden und Übergabeparameter

Ein sehr umfangreiches Kapitel mit einigen Stolpersteinen ist das Thema HTTP Methoden (GET, PUT, POST, DELETE) in Kombination mit Übergabeparametern. Die Swagger OpenApi Spezifikation gibt bereits einige Regeln vor, welche Kombinationen vorgesehen sind. Version 2 und Version 3 definieren unterschiedliche Parameter Locations. Hier soll es vor allem um die folgenden (V2) Locations gehen.

  • Query – Parameters that are appended to the URL. For example, in /items?id=###, the query parameter is id.
  • Path – Used together with Path Templating, where the parameter value is actually part of the operation’s URL. This does not include the host or base path of the API. For example, in /items/{itemId}, the path parameter is itemId.
  • Body – The payload that’s appended to the HTTP request. Since there can only be one payload, there can only be onebody parameter. The name of the body parameter has no effect on the parameter itself and is used for documentation purposes only. Since Form parameters are also in the payload, body and form parameters cannot exist together for the same operation.

Mögliche Kombinationen HTTP Methoden und Parameter Locations

Body Path Query
GET ⚠ Compilerfehler
PUT
POST
DELETE ⚠ Compilerfehler

Beim Generieren mit TSOA kommt es bei der Kombination GET + Body und DELETE + Body zu folgenden Compilerfehlern. Diese Kombination wird nicht unterstützt.

[generate.swagger] Generate swagger error.
[generate.swagger] Generate routes error.
[generate.swagger] Error: @Body('value') Can't support in GET method.
[generate.swagger] Generate swagger error.
[generate.swagger] Generate routes error.
[generate.swagger] Error: @Body('value') Can't support in DELETE method.

Mehrere Übergabeparameter in Kombination

Die Swagger OpenApi Spezifikation erlaubt und TSOA unterstützt die Kombination unterschiedlicher Parameter Locations an einer Service Funktion.

An einer Service Funktion können mehrere Eingaben und Ausgaben definiert werden.

  • mehrere Path Parameter möglich
  • mehrere Query Parameter möglich
  • ein Body Parameter möglich
  • ein Result möglich
  • ein Status Codes für erfolgreiche Ausführung möglich
  • mehrere Status Codes für fehlerhafte Ausführung möglich

Die Reihenfolge der Parameter ist beliebig. Die Lesbarkeit profitiert aber von einer Path > Query > Body Reihenfolge. Zum Bespiel:

@SuccessResponse('204', 'successful')
@Response('400', 'invalid input')
@Response('404', 'not found')
@Response('407', 'unknown error')
@Put('{nr}')
public async park (@Path() nr: number, @Query() days: number, @Query() start: Date, @Body() car: Car): Promise {
    // ...
}
curl -X PUT "http://localhost:9091/v1/Garage/2?days=14&start=2018-05-22T08%3A32%3A46.371Z" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"plateNumber\": \"dd-ab 123\", \"manufacturer\": \"BMW\", \"model\": \"Z1\"}"

Pro Service Funktion ist nur ein Body Parameter zulässig

Während mehrere Eingabeparameter pro Servicefunktion per Path und Query übergeben werden können, darf pro Funktion nur ein Body Parameter angegeben werden. Folgendes Beispiel erzeugt beim Generieren der Service Schnittstelle mit TSOA einen Fehler.

@Put('store')
public async setP3B (@Body() text?: string, @Body() zahl?: number, @Body() wahr?: boolean): Promise {
  // ...
}
Generate swagger error.
Error: Only one body parameter allowed in 'TestController.setP3B' method.

Parameter Locations und Datentypen

Die Service Schnittstellengenerierung von TSOA erlaubt nur bestimmte Kombinationen von Parameter Location und Datentyp.

Query Path Body
number
boolean
string
Date
[ ] Array
{ v:t } Object
Enum
  • da Query und Path Parameter in der URL übertragen werden, können alle primitiven Datentypen verwendet werden (number, boolean, string) ✅
  • Date Werte werden von der generierten Schnittstelle wie Text / string behandelt, daher können sie als Query und Path Parameter als Teil der URL verwendet werden. ✅
  • Enum Werte werden von der generierten Schnittstele wie Text / string behandelt, daher können sie als Query und Path Parameter als Teil der URL verwendet werden. ✅
  • da Body Parameter als JSON Objekte interpretiert werden, können keine primitiven Datentypen und Enums als Body verwendet werden ⚠
  • Arrays (z.B. number[]) können nicht als URL Pfad Element übertragen werden ❌
  • Arrays können als Query oder Body JSON Objekt übertragen werden ✅
  • JSON Objekte können nicht als Teil der URL übertragen werden ❌
  • JSON Objekte können nur als Body Parameter übertragen werden ✅

Objekte als Query oder Path Parameter

Wird bei einer Service Funktion versehentlich ein (JSON) Objekt als Datentyp für einen Query oder Path Parameter angegeben, bricht TSOA die Generierung der Service Schnittstelle mit einem aussagkräftigen Fehler ab. ❌

[generate.swagger] Generate swagger error.
[generate.swagger]  Error: @Path('param') Can't support 'refObject' type.
[generate.swagger]  in 'CarController.functionWithPathObject'
[generate.swagger] Generate swagger error.
[generate.swagger]  Error: @Query('param') Can't support 'refObject' type.
[generate.swagger]  in 'CarController.functionWithQueryObject'

Array als Path Parameter

Wird bei einer Service Funktion versehentlich ein Array (z.B. number[]) als Path Parameter angegeben, bricht TSOA die Generierung der Service Schnittstelle mit einem aussagekräftigen Fehler ab. ❌

[generate.routes] Generate routes error.
[generate.routes]  Error: @Path('param') Can't support 'array' type.
[generate.routes]  in 'CarController.functionWithPathArray

Enum als Body Parameter

Wird bei einer Service Funktion versehentlich ein Enum als Body Parameter angegeben, funktioniert die Generierung mit TSOA fehlerfrei. Beim Aufruf der Service Funktion mit einem Enum Wert wird der Aufruf mit einem Fehler abgebrochen. D.h. der Fehler tritt erst zur Laufzeit des generierten Dienstes auf. ❌

SyntaxError: Unexpected token # in JSON at position 0
    at JSON.parse ()
    at createStrictSyntaxError (/node_modules/body-parser/lib/types/json.js:157:10)

Primitive Übergabeparameter und Body

Die Verwendung von primitiven Datentypen (string, number, boolean, Date) als Body Parameter ist grundsätzlich möglich. TSOA generiert ohne Warnung eine passende Service Schnittstelle mit Swagger Beschreibung (swagger.json).

Die Verwendung von primitiven Übergabeparametern als Body Parameter kann zu Ausführungsfehlern des generierten Dienstes führen! ⚠

Im folgenden Beispiel wurden Setter mit verschiedenen Body Datentypen deklariert. Die TSOA Codegenerierung verläuft fehlerfrei und ohne Warnungen.

@Route('Car')
export class CarController extends Controller {
  constructor () {
    super()
  }

  @Tags('Car')
  @SuccessResponse('200', 'successful')
  @Put('{plateNr}/color')
  public async setColor (@Path('plateNr') plate: string, @Body() color: string): Promise<void> {
    // ...
    // no compile error, no warning
  }

  @Tags('Car')
  @SuccessResponse('200', 'successful')
  @Put('{plateNr}/nextInspection')
  public async setNextInspection (@Path('plateNr') plate: string, @Body() date: Date): Promise<void> {
    // ...
    // no compile error, no warning
  }

  @Tags('Car')
  @SuccessResponse('200', 'successful')
  @Put('{plateNr}/mileage')
  public async setMileage (@Path('plateNr') plate: string, @Body() miles: number): Promise<void> {
    // ...
    // no compile error, no warning
  }

  @Tags('Car')
  @SuccessResponse('200', 'successful')
  @Put('{plateNr}/repair')
  public async setInRepair (@Path('plateNr') plate: string, @Body() repair: boolean): Promise<void> {
    // ...
    // no compile error, no warning
  }

  @Tags('Car')
  @SuccessResponse('200', 'successful')
  @Put('{plateNr}/parts')
  public async setPartsIds (@Path('plateNr') plate: string, @Body() ids: number[]): Promise<void> {
    // ...
    // no compile error, no warning
  }

  @Tags('Car')
  @SuccessResponse('200', 'successful')
  @Put('{plateNr}/weekdayCleaning')
  public async setCleaningDays (@Path('plateNr') plate: string, @Body() days: boolean[]): Promise<void> {
    // ...
    // no compile error, no warning
  } 
}

Über die Swagger Console kann eine HTML Beschreibung und Testoberfläche für die generierte Schnittstelle mit deren Funktionen angezeigt werden.

  • setColor, setNextInspection, setMileage, setInRepair besitzen kein Eingabefeld für den Wert, die Funktionen lassen sich nicht ausführen ⚠
  • setPartsIds, setCleaningDays besitzen ein Eingabefeld für das Array und die Funktionen lassen sich ausführen ✅

Beim Aufrufen von setColor, setNextInspection, setMileage, setInRepair über die URL wird ein Fehler zurückgegeben. ❌

curl -X PUT "http://localhost:9091/v1/Car/color" -H "accept: application/json" -H "Content-Type: application/json" -d "blue"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>SyntaxError: Unexpected token # in JSON at position 0<br> &nbsp; &nbsp;at JSON.parse (&lt;anonymous&gt;)<br> &nbsp; &nbsp;at createStrictSyntaxError
[main] INFO io.swagger.parser.Swagger20Parser - reading from http://localhost:9091/swagger.json
Exception in thread "main" java.lang.RuntimeException: Could not process operation:
  Tag: Tag {
    name: Car
    description: null
    externalDocs: null
    extensions:{}}
  Operation: SetColor
  Resource: put /Car/{plateNr}/color
  Definitions: {Car=io.swagger.models.ModelImpl@1c162f33, Garage=io.swagger.models.ModelImpl@c189fc22, IVersion=io.swagger.models.ModelImpl@431c44ff, UpdateImage=io.swagger.models.ModelImpl@2351e278, NetworkingMode=io.swagger.models.ModelImpl@f30acc6, NetworkConfig=io.swagger.models.ModelImpl@776cc6de, SysInfo=io.swagger.models.ModelImpl@b02fa48a}
  Exception: null
    at io.swagger.codegen.DefaultGenerator.processOperation(DefaultGenerator.java:916)
    at io.swagger.codegen.DefaultGenerator.processPaths(DefaultGenerator.java:793)
    at io.swagger.codegen.DefaultGenerator.generateApis(DefaultGenerator.java:418)
    at io.swagger.codegen.DefaultGenerator.generate(DefaultGenerator.java:730)
    at io.swagger.codegen.cmd.Generate.run(Generate.java:285)
    at io.swagger.codegen.SwaggerCodegen.main(SwaggerCodegen.java:35)
Caused by: java.lang.NullPointerException
    at io.swagger.codegen.DefaultCodegen.isDataTypeBinary(DefaultCodegen.java:2734)
    at io.swagger.codegen.DefaultCodegen.fromParameter(DefaultCodegen.java:2685)
    at io.swagger.codegen.DefaultCodegen.fromOperation(DefaultCodegen.java:2241)
    at io.swagger.codegen.DefaultGenerator.processOperation(DefaultGenerator.java:864)
    ... 5 more
Wichtig!
Niemals primitive Datentypen als Body-Parameter verwenden! Die Werte können nicht zuverlässig an die Funktion des Service übergeben werden. Body Parameter sollten nur für Objekte und Arrays verwendet werden.

Primitive Datentypen (und Felder) müssen als Body-Parameter in JSON Objekte verpackt werden!

Um das obere Beispiel lauffähig zu bekommen, gibt es einen einfachen Weg. Die primitiven Daten sollten in Objekte verpackt werden. Im folgenden Sample Code wurden für die primitiven Datentypen einfache Wrapper Objekte angelegt.

  • TextInput für string Parameter
  • NumberInput für number Parameter
  • BoolInput für boolean Parameter
  • DateInput für Date Parameter
  • NumberArrayInput für number[] Parameter
  • BooleanArrayInput für boolean[] Parameter

Der verbesserte Samplecode.

interface TextInput {
  value: string
}

interface NumberInput {
  value: number
}

interface BoolInput {
  value: boolean
}

interface DateInput {
  value: Date
}

interface NumberArrayInput {
  value: number[]
}

interface BoolArrayInput {
  value: boolean[]
}

@Route('Car')
@injectable()
export class CarController extends Controller {
  constructor () {
    super()
  }

  @Tags('Car')
  @SuccessResponse('200', 'successful')
  @Put('{plateNr}/color')
  public async setColor (@Path('plateNr') plate: string, @Body() color: TextInput): Promise<void> {
    // ...
  }

  @Tags('Car')
  @SuccessResponse('200', 'successful')
  @Put('{plateNr}/nextInspection')
  public async setNextInspection (@Path('plateNr') plate: string, @Body() date: DateInput): Promise<void> {
    // ...
  }

  @Tags('Car')
  @SuccessResponse('200', 'successful')
  @Put('{plateNr}/mileage')
  public async setMileage (@Path('plateNr') plate: string, @Body() miles: NumberInput): Promise<void> {
    // ...
  }

  @Tags('Car')
  @SuccessResponse('200', 'successful')
  @Put('{plateNr}/repair')
  public async setInRepair (@Path('plateNr') plate: string, @Body() repair: BoolInput): Promise<void> {
    // ...
  }

  @Tags('Car')
  @SuccessResponse('200', 'successful')
  @Put('{plateNr}/parts')
  public async setPartsIds (@Path('plateNr') plate: string, @Body() ids: NumberArrayInput): Promise<void> {
    // ...
  }

  @Tags('Car')
  @SuccessResponse('200', 'successful')
  @Put('{plateNr}/weekdayCleaning')
  public async setCleaningDays (@Path('plateNr') plate: string, @Body() days: BoolArrayInput): Promise<void> {
    // ...
  }
}

Über die Swagger Console kann eine HTML Beschreibung und Testoberfläche für die generierte Schnittstelle mit deren Funktionen angezeigt werden.

  • für alle Funktionen sind alle benötigten Eingabefelder vorhanden, die Funktionen können über die Oberfläche ausgeführt werden ✅
  • die native Eingabeunterstützung für boolean, number, string und Date ist in der Oberfläche nicht nutzbar ❌

Ein Aufruf der primitiven Daten Setter ist jetzt auch über die URL möglich. Der Aufruf liefert Status Code 204 für eine erfolgreiche Ausführung zurück.

curl -X PUT "http://localhost:9091/v1/Car/DDAB123/color" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"value\": \"blue\"}"

Parameter Locations und optionale Parameter

Optionale Parameter werden je nach Parameter Location unterschiedlich unterstützt.

Query ✅ vollständige Unterstützung
Path ⚠ wird implizit als required markiert, Wert wird immer erwartet
Body ❌ Fehler bei Ausführung, Wert wird immer erwartet

TSOA gestattet für alle Parameter Locations die Definition als optionalen Parameter. Die Generierung verläuft ohne Fehler und Warnungen.

@Tags('Garage')
@Put('{nr}/{plate}')
public async storeCar (@Body() car?: Car, @Path('plate') plate?: string, @Path('nr') nr?: number, @Query('lock') lock?: boolean): Promise<void> {
    // ...
}
		"/Car/{nr}/{plate}": {
			"put": {
				"operationId": "StoreCar",
				"produces": [
					"application/json"
				],
				"responses": {
					"204": {
						"description": "No content"
					}
				},
				"tags": [
					"Garage"
				],
				"security": [],
				"parameters": [
					{
						"in": "body",
						"name": "car",
						"required": false,
						"schema": {
							"$ref": "#/definitions/Car"
						}
					},
					{
						"in": "path",
						"name": "plate",
						"required": true,
						"type": "string"
					},
					{
						"in": "path",
						"name": "nr",
						"required": true,
						"format": "double",
						"type": "number"
					},
					{
						"in": "query",
						"name": "lock",
						"required": false,
						"type": "boolean"
					}
				]
			}
		}

Nach der Generierung des Services sind die optionalen Query Parameter auch in der Schnittstellenbeschreibung als optional markiert ("required": false) ✅. Body und Path Parameter werden jedoch als required / benötigt behandelt ⚠.

Die generierte Swagger Beschreibung der Funktion zeigt, dass die Path Parameter nr und plate nicht mehr optional sind ("required": true) ⚠. TSOA ändert das automatisch für Path Parameter während der Codegenerierung.

Auch der Body Parameter ist in der Praxis nicht mehr optional sondern muss angegeben werden ❌. Wird die obere Funktion ohne car aufgerufen, dann wird ein Fehler zurückgemeldet.

  • ohne Body Parameter wird Status Code 400 (Error: Bad Request) zurückgegeben
  • ohne Body Parameter wird eine kryptische HTML Fehlermeldung zurückgegeben
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>[object Object]</pre>
</body>
</html>
Fazit
Path und BodyParameter können nicht als optional deklariert werden.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.