TSOA Fallstricke: Service URL Shadowing (Deutsch)

TSOA Fallstricke: Service URL Shadowing (Deutsch)

Dies ist der zweite 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 wie sich per TSOA erzeugte REST Funktionen überdecken können (Shadowing) und wie dieser Fehler vermieden werden kann.


Service URL Shadowing

Die Reihenfolge von Servicefunktionen bzw. Service URLs in einer TSOA Controller-Klasse ist relevant für die Auswertung von Anfragen an den Dienst. TSOA liest beim Generieren der Serverkomponente alle importierten Controller ein. Aus den eingelesenen Funktionen, verwendeten Modelklassen und Annotationen wird ein Routing erzeugt. Dabei werden die Service URLs in der gleichen Reihenfolge wie in der Typescript Controller Klasse registriert.

Durch diese festgelegte Reihenfolge der Service URLs kann es bei Verwendung von Path Parametern zum Überdecken (Shadowing) von Service Pfaden und damit von Service Funktionen kommen. Das Problem tritt z.B. auf, wenn eine Funktion mit einem variablen Pfad vor einer Funktion, welche anstelle des variablen Pfadelements ein festes Schlüsselwort verwendet, definiert wird. In solchen Fällen wird die überdeckte Funktion nie aufgerufen und alle Funktionsaufrufe an diese von der zuerst definierten Funktion abgefangen.

Im folgenden TypeScript Beispiel ist das Problem demonstriert.

@Route('Garage')
@injectable()
export class GarageController extends Controller {
  constructor () {
    super()
  }

  @Tags('Garage')
  @SuccessResponse('200', 'successful')
  @Get()
  public async getGarages (): Promise<Garage[]> {
    return this.getAllGarages()
  }

  @Tags('Garage')
  @SuccessResponse('200', 'successful')
  @Response('404', 'not found')
  @Get('{nr}')
  public async getGarage (@Path() nr: number): Promise {
    return this.getInternalGarage(nr)
  }

  @Tags('Garage')
  @SuccessResponse('200', 'successful')
  @Response('404', 'not found')
  @Get('{nr}/count')
  public async countCars (@Path() nr: number): Promise {
    const garage = await this.getInternalGarage(nr)

    return garage.cars.length
  }

  @Tags('Garage')
  @SuccessResponse('200', 'successful')
  @Response('404', 'not found')
  @Get('count') // Shadowing is happening here!!!
  public async countAllCars (): Promise {
    const garages = await this.getAllGarages()
    let result = 0
    for (const gar of garages) {
      result += gar.cars.length
    }

    return result
  }

// ...
}

Das Beispiel wird mit TSOA fehlerfrei und ohne Warnungen compiliert und erzeugt einen funktionierenden Dienst mit REST Schnittstelle.

Beim Aufruf der Funktion countAllCars() kommt es trotzdem zu einem Fehler ⚠.

  • Aufruf über Browser URL = http://localhost:9091/v1/Garage/count
  • zurückgemeldeter Fehler = 400 (Error: Bad Request)

Der Aufruf gibt folgende Fehlermeldung als HTML zurück.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>[object Object]</pre>
</body>
</html>

Was ist hier passiert? Warum wird Status Code 400 zurückgegeben? Die Funktion countAllCars() besitzt doch gar keinen Eingabeparameter. Und was bedeutet object Object in der Fehlermeldung?

Die Antwort ist, dass die falsche Funktion aufgerufen wurde. Statt countAllCars() wurde getGarage() aufgerufen.

Das Problem wird leichter verständlich, wenn die Reihenfolge der Servicefunktionen mit deren Service URLs betrachtet wird.

die falsche Reihenfolge der Servicefunktionen verursacht Zuordnungskonflikte
  • Der Aufruf http://localhost:9091/v1/Garage/count wurde an http://localhost:9091/v1/Garage/{nr} weitergeleitet und damit die Funktion getGarage() statt countAllCars() aufgerufen. 💡
  • Obwohl die Funktion getGarage() als Parameter eine Zahl erwartet, wurde sie aufgerufen, da sie vor countAllCars() definiert wurde und ihr URL Muster zur Aufruf URL passt.
  • Der URL Parser kann hier nicht unterscheiden, ob der variable Pfad-Teil eine Zahl, Datum, Wahrheitswert sein soll. Er behandelt den variablen Teil immer zuerst als Text / String.
  • Das Schlüsselwort „count“ wird im nächsten Schritt von der Service Schnittstelle zur Zahl konvertiert. Das schlägt mit einem Status Code 400 fehl. Da TSOA standardmäßig nur einen sehr einfachen (HTML) Fehlerhandler in der Service Schnittstelle vorsieht, wird eine kryptische HTML Fehlermeldung zurückgegeben, die keinen Aufschluss auf den eigentlichen Fehler zulässt.
  • Die Funktion getGarage() wird nicht aufgerufen. Der Fehler wird noch vor dem Aufruf der Methode abgefangen.

Eine einfache Umstellung der Reihenfolge behebt das URL Shadowing

Durch ein Verschieben der überdeckten Servicefunktion vor die überdeckende Funktion kann das problematische Shadowing behoben werden. Im Beispiel muss die Funktion countAllCars() vor getGarage() verschoben werden.

@Route('Garage')
@injectable()
export class GarageController extends Controller {
  constructor () {
    super()
  }
 
  @Tags('Garage')
  @SuccessResponse('200', 'successful')
  @Get()
  public async getGarages (): Promise<Garage[]> {
    return this.getAllGarages()
  }
 
  @Tags('Garage')
  @SuccessResponse('200', 'successful')
  @Response('404', 'not found')
  @Get('count')
  public async countAllCars (): Promise { // moved to solve shadowing
    const garages = await this.getAllGarages()
    let result = 0
    for (const gar of garages) {
      result += gar.cars.length
    }
 
    return result
  }
 
  @Tags('Garage')
  @SuccessResponse('200', 'successful')
  @Response('404', 'not found')
  @Get('{nr}')
  public async getGarage (@Path() nr: number): Promise {
    return this.getInternalGarage(nr)
  }
 
  @Tags('Garage')
  @SuccessResponse('200', 'successful')
  @Response('404', 'not found')
  @Get('{nr}/count')
  public async countCars (@Path() nr: number): Promise {
    const garage = await this.getInternalGarage(nr)
 
    return garage.cars.length
  }
// ...
}

Die Verschiebung spiegelt sich nach der Neugenerierung der Service Schnittstelle mit TSOA sofort wieder.

die Funktion mit Schlüsselwort wurde vor die Funktion mit Parameter verschoben
  • http://localhost:9091/v1/Garage/count ruft stets countAllCars() auf
  • http://localhost:9091/v1/Garage/5 bzw. mit anderen Zahlen ruft stets getGarage() auf
  • http://localhost:9091/v1/Garage/5/count ruft stets countCars() auf
  • http://localhost:9091/v1/Garage/somethingelse bzw. ein beliebiger Eingabetext führt zu Fehler mit Status Code 404

Was ist zu beachten um URL Shadowing zu vermeiden?

  • URL Shadowing tritt nur bei Service Funktionen mit gleichen HTTP Methoden (GET, PUT, POST, DELETE) auf. D.h. gleiche URLs mit unterschiedlichen HTTP Methoden überdecken sich nicht.
  • URL Shadowing tritt nur bei Verwendung von Path / Pfad Parametern auf.

Um das Problem generell zu vermeiden, bieten sich einige grundlegende Regeln an.

  1. dynamische Pfad-Elemente unter statische Pfad-Elemente
  2. URL mit wenigen Pfad-Elementen unter URLs mit vielen Pfad-Elementen (nicht zwingend, nur zur besseren Ordnung)

An einem funktionierendem Beispiel:

  • /x/m/n/{u}
  • /x/{a}/y/{b}/z
  • /x/{a}
  • /x

Schreibe einen Kommentar

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