diff --git a/deploy/plugins/index.js b/deploy/plugins/index.js
index 9f15b4687a85be1f07b76284694132fd0fb799eb..f0216dea9b1aa14f7a85dfc955a11b530e9f740e 100644
--- a/deploy/plugins/index.js
+++ b/deploy/plugins/index.js
@@ -5,15 +5,11 @@
 
 const express = require('express')
 const router = express.Router()
-const PLUGIN_URLS = process.env.PLUGIN_URLS && JSON.stringify(process.env.PLUGIN_URLS.split(';'))
+const PLUGIN_URLS = (process.env.PLUGIN_URLS && process.env.PLUGIN_URLS.split(';'))
+  || []
 
 router.get('', (_req, res) => {
-
-  if (PLUGIN_URLS) {
-    return res.status(200).send(PLUGIN_URLS)
-  } else {
-    return res.status(200).send('[]')
-  }
+  return res.status(200).json(PLUGIN_URLS)
 })
 
 module.exports = router
\ No newline at end of file
diff --git a/src/atlasViewer/atlasViewer.pluginService.service.spec.ts b/src/atlasViewer/atlasViewer.pluginService.service.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..51e3fb8f8f6cd33c05c06867a4c2507ec3c829bf
--- /dev/null
+++ b/src/atlasViewer/atlasViewer.pluginService.service.spec.ts
@@ -0,0 +1,108 @@
+import { PluginServices } from "./atlasViewer.pluginService.service";
+import { TestBed, inject } from "@angular/core/testing";
+import { MainModule } from "src/main.module";
+import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'
+
+const MOCK_PLUGIN_MANIFEST = {
+  name: 'fzj.xg.MOCK_PLUGIN_MANIFEST',
+  templateURL: 'http://localhost:10001/template.html',
+  scriptURL: 'http://localhost:10001/script.js'
+}
+
+describe('PluginServices', () => {
+  let pluginService: PluginServices
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [
+        HttpClientTestingModule,
+        MainModule
+      ]
+    }).compileComponents()
+
+    pluginService = TestBed.get(PluginServices)
+  })
+
+  it(
+    'is instantiated in test suite OK',
+    () => expect(TestBed.get(PluginServices)).toBeTruthy()
+  )
+
+  it(
+    'expectOne is working as expected',
+    inject([HttpTestingController], (httpMock: HttpTestingController) => {
+      expect(httpMock.match('test').length).toBe(0)
+      pluginService.fetch('test')
+      expect(httpMock.match('test').length).toBe(1)
+      pluginService.fetch('test')
+      pluginService.fetch('test')
+      expect(httpMock.match('test').length).toBe(2)
+    })
+  )
+
+  describe('#launchPlugin', () => {
+
+    describe('basic fetching functionality', () => {
+      it(
+        'fetches templateURL and scriptURL properly',
+        inject([HttpTestingController], (httpMock: HttpTestingController) => {
+  
+          pluginService.launchPlugin(MOCK_PLUGIN_MANIFEST)
+    
+          const mockTemplate = httpMock.expectOne(MOCK_PLUGIN_MANIFEST.templateURL)
+          const mockScript = httpMock.expectOne(MOCK_PLUGIN_MANIFEST.scriptURL)
+    
+          expect(mockTemplate).toBeTruthy()
+          expect(mockScript).toBeTruthy()
+        })
+      )
+  
+      it(
+        'template overrides templateURL',
+        inject([HttpTestingController], (httpMock: HttpTestingController) => {
+          pluginService.launchPlugin({
+            ...MOCK_PLUGIN_MANIFEST,
+            template: ''
+          })
+          
+          httpMock.expectNone(MOCK_PLUGIN_MANIFEST.templateURL)
+          const mockScript = httpMock.expectOne(MOCK_PLUGIN_MANIFEST.scriptURL)
+    
+          expect(mockScript).toBeTruthy()
+        })
+      )
+  
+      it(
+        'script overrides scriptURL',
+  
+        inject([HttpTestingController], (httpMock: HttpTestingController) => {
+          pluginService.launchPlugin({
+            ...MOCK_PLUGIN_MANIFEST,
+            script: ''
+          })
+          
+          const mockTemplate = httpMock.expectOne(MOCK_PLUGIN_MANIFEST.templateURL)
+          httpMock.expectNone(MOCK_PLUGIN_MANIFEST.scriptURL)
+    
+          expect(mockTemplate).toBeTruthy()
+        })
+      )
+    })
+
+    describe('racing slow cconnection when launching plugin', () => {
+      it(
+        'when template/script has yet been fetched, repeated launchPlugin should not result in repeated fetching',
+        inject([HttpTestingController], (httpMock:HttpTestingController) => {
+
+          expect(pluginService.pluginLaunching.has(MOCK_PLUGIN_MANIFEST.name)).toBeFalsy()
+          pluginService.launchPlugin(MOCK_PLUGIN_MANIFEST)
+          pluginService.launchPlugin(MOCK_PLUGIN_MANIFEST)
+          expect(httpMock.match(MOCK_PLUGIN_MANIFEST.scriptURL).length).toBe(1)
+          expect(httpMock.match(MOCK_PLUGIN_MANIFEST.templateURL).length).toBe(1)
+
+          expect(pluginService.pluginLaunching.has(MOCK_PLUGIN_MANIFEST.name)).toBeTruthy()
+        })
+      )
+    })
+  })
+})
\ No newline at end of file
diff --git a/src/atlasViewer/atlasViewer.pluginService.service.ts b/src/atlasViewer/atlasViewer.pluginService.service.ts
index 9d9ac412464dae44465963da913932e71cc608db..723adfea2da1145acbf817b0aa2593414cf772a0 100644
--- a/src/atlasViewer/atlasViewer.pluginService.service.ts
+++ b/src/atlasViewer/atlasViewer.pluginService.service.ts
@@ -1,4 +1,5 @@
 import { Injectable, ViewContainerRef, ComponentFactoryResolver, ComponentFactory } from "@angular/core";
+import { HttpClient } from '@angular/common/http'
 import { PluginInitManifestInterface, ACTION_TYPES } from "src/services/state/pluginState.store";
 import { isDefined } from 'src/services/stateStore.service'
 import { AtlasViewerAPIServices } from "./atlasViewer.apiService.service";
@@ -23,13 +24,20 @@ export class PluginServices{
   public appendSrc : (script:HTMLElement)=>void
   public removeSrc: (script:HTMLElement) => void
   private pluginUnitFactory : ComponentFactory<PluginUnit>
+  public minimisedPlugins$ : Observable<Set<string>>
+
+  /**
+   * TODO remove polyfil and convert all calls to this.fetch to http client
+   */
+  public fetch: (url:string) => Promise<any> = (url) => this.http.get(url).toPromise()
 
   constructor(
     private apiService : AtlasViewerAPIServices,
     private constantService : AtlasViewerConstantsServices,
     private widgetService : WidgetServices,
     private cfr : ComponentFactoryResolver,
-    private store : Store<PluginInitManifestInterface>
+    private store : Store<PluginInitManifestInterface>,
+    private http: HttpClient
   ){
 
     this.pluginUnitFactory = this.cfr.resolveComponentFactory( PluginUnit )
@@ -44,17 +52,17 @@ export class PluginServices{
          * PLUGINDEV should return an array of 
          */
         PLUGINDEV
-          ? fetch(PLUGINDEV).then(res => res.json())
+          ? this.fetch(PLUGINDEV).then(res => res.json())
           : Promise.resolve([]),
         new Promise(resolve => {
-          fetch(`${this.constantService.backendUrl}plugins`)
+          this.fetch(`${this.constantService.backendUrl}plugins`)
             .then(res => res.json())
             .then(arr => Promise.all(
               arr.map(url => new Promise(rs => 
                 /**
                  * instead of failing all promises when fetching manifests, only fail those that fails to fetch
                  */
-                fetch(url).then(res => res.json()).then(rs).catch(e => (console.log('fetching manifest error', e), rs(null))))
+                this.fetch(url).then(res => res.json()).then(rs).catch(e => (console.log('fetching manifest error', e), rs(null))))
               )
             ))
             .then(manifests => resolve(
@@ -67,7 +75,7 @@ export class PluginServices{
         Promise.all(
           BUNDLEDPLUGINS
             .filter(v => typeof v === 'string')
-            .map(v => fetch(`res/plugin_examples/${v}/manifest.json`).then(res => res.json()))
+            .map(v => this.fetch(`res/plugin_examples/${v}/manifest.json`).then(res => res.json()))
         )
           .then(arr => arr.reduce((acc,curr) => acc.concat(curr) ,[]))
       ])
@@ -112,14 +120,14 @@ export class PluginServices{
         isDefined(plugin.template) ?
           Promise.resolve('template already provided') :
           isDefined(plugin.templateURL) ?
-            fetch(plugin.templateURL)
+            this.fetch(plugin.templateURL)
               .then(res=>res.text())
               .then(template=>plugin.template = template) :
             Promise.reject('both template and templateURL are not defined') ,
         isDefined(plugin.script) ?
           Promise.resolve('script already provided') :
           isDefined(plugin.scriptURL) ?
-            fetch(plugin.scriptURL)
+            this.fetch(plugin.scriptURL)
               .then(res=>res.text())
               .then(script=>plugin.script = script) :
             Promise.reject('both script and scriptURL are not defined') 
@@ -135,32 +143,53 @@ export class PluginServices{
     this.launchedPlugins.add(pluginName)
     this.launchedPlugins$.next(this.launchedPlugins)
   }
-  removePluginToLaunchedSet(pluginName:string){
+  removePluginFromLaunchedSet(pluginName:string){
     this.launchedPlugins.delete(pluginName)
     this.launchedPlugins$.next(this.launchedPlugins)
   }
 
+  
+  pluginIsLaunching(pluginName:string){
+    return this.launchingPlugins.has(pluginName)
+  }
+  addPluginToIsLaunchingSet(pluginName:string) {
+    this.launchingPlugins.add(pluginName)
+  }
+  removePluginFromIsLaunchingSet(pluginName:string){
+    this.launchedPlugins.delete(pluginName)
+  }
+
   private mapPluginNameToWidgetUnit: Map<string, WidgetUnit> = new Map()
 
   pluginIsMinimised(pluginName:string) {
     return this.widgetService.isMinimised( this.mapPluginNameToWidgetUnit.get(pluginName) )
   }
 
-  public minimisedPlugins$: Observable<Set<string>>
-
+  private launchingPlugins: Set<string> = new Set()
   public orphanPlugins: Set<PluginManifest> = new Set()
   launchPlugin(plugin:PluginManifest){
-    if(this.apiService.interactiveViewer.pluginControl[plugin.name]){
-      // if already launched, toggle minimise/restore
+    if (this.pluginIsLaunching(plugin.name)) {
+      // plugin launching please be patient
+      // TODO add visual feedback
+      return
+    }
+    if ( this.pluginHasLaunched(plugin.name)) {
+      // plugin launched
+      // TODO add visual feedback
 
+      // if widget window is minimized, maximize it
+      
       const wu = this.mapPluginNameToWidgetUnit.get(plugin.name)
       if (this.widgetService.isMinimised(wu)) {
         this.widgetService.unminimise(wu)
       } else {
         this.widgetService.minimise(wu)
       }
-      return Promise.reject('plugin already launched')
+      return
     }
+
+    this.addPluginToIsLaunchingSet(plugin.name)
+    
     return this.readyPlugin(plugin)
       .then(()=>{
         const pluginUnit = this.pluginViewContainerRef.createComponent( this.pluginUnitFactory )
@@ -199,7 +228,7 @@ export class PluginServices{
 
         const shutdownCB = [
           () => {
-            this.removePluginToLaunchedSet(plugin.name)
+            this.removePluginFromLaunchedSet(plugin.name)
           }
         ]
 
@@ -228,6 +257,8 @@ export class PluginServices{
         })
 
         this.addPluginToLaunchedSet(plugin.name)
+        this.removePluginFromIsLaunchingSet(plugin.name)
+
         this.mapPluginNameToWidgetUnit.set(plugin.name, widgetCompRef.instance)
 
         const unsubscribeOnPluginDestroy = []
diff --git a/src/ui/ui.module.ts b/src/ui/ui.module.ts
index 694000b8e0295bf7d27ba038cb189821b2cabd17..7e69c597b1dfb9e7828b9fcb9dd82d711a090757 100644
--- a/src/ui/ui.module.ts
+++ b/src/ui/ui.module.ts
@@ -51,9 +51,12 @@ import { AppendtooltipTextPipe } from "src/util/pipes/appendTooltipText.pipe";
 import { MenuIconPluginBtnClsPipe } from "src/util/pipes/menuIconPluginBtnCls.pipe";
 import { MenuIconKgSearchBtnClsPipe } from "src/util/pipes/menuIconKgSearchBtnCls.pipe";
 import { ScrollingModule } from "@angular/cdk/scrolling"
+import { HttpClientModule } from "@angular/common/http";
+
 
 @NgModule({
   imports : [
+    HttpClientModule,
     FormsModule,
     LayoutModule,
     ComponentsModule,